@morsa/guidance-bank 0.2.0

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.
Files changed (95) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +135 -0
  3. package/bin/gbank.js +3 -0
  4. package/dist/cli/commands/init.js +27 -0
  5. package/dist/cli/commands/mcpServe.js +8 -0
  6. package/dist/cli/commands/stats.js +92 -0
  7. package/dist/cli/index.js +67 -0
  8. package/dist/cli/postinstall.js +24 -0
  9. package/dist/cli/prompts/initPrompts.js +89 -0
  10. package/dist/cli/prompts/providerAvailability.js +31 -0
  11. package/dist/core/audit/summarizeEntryContent.js +43 -0
  12. package/dist/core/audit/types.js +1 -0
  13. package/dist/core/bank/canonicalEntry.js +106 -0
  14. package/dist/core/bank/integration.js +16 -0
  15. package/dist/core/bank/layout.js +159 -0
  16. package/dist/core/bank/lifecycle.js +24 -0
  17. package/dist/core/bank/manifest.js +37 -0
  18. package/dist/core/bank/project.js +105 -0
  19. package/dist/core/bank/types.js +6 -0
  20. package/dist/core/context/contextEntryResolver.js +140 -0
  21. package/dist/core/context/contextTextRenderer.js +116 -0
  22. package/dist/core/context/detectProjectContext.js +118 -0
  23. package/dist/core/context/resolveContextService.js +138 -0
  24. package/dist/core/context/types.js +1 -0
  25. package/dist/core/init/initService.js +112 -0
  26. package/dist/core/init/initTypes.js +1 -0
  27. package/dist/core/projects/createBankDeriveGuidance/index.js +47 -0
  28. package/dist/core/projects/createBankDeriveGuidance/shared/general.js +49 -0
  29. package/dist/core/projects/createBankDeriveGuidance/shared/typescript.js +18 -0
  30. package/dist/core/projects/createBankDeriveGuidance/stacks/angular.js +252 -0
  31. package/dist/core/projects/createBankDeriveGuidance/stacks/ios.js +254 -0
  32. package/dist/core/projects/createBankDeriveGuidance/stacks/nextjs.js +220 -0
  33. package/dist/core/projects/createBankDeriveGuidance/stacks/nodejs.js +221 -0
  34. package/dist/core/projects/createBankDeriveGuidance/stacks/other.js +34 -0
  35. package/dist/core/projects/createBankDeriveGuidance/stacks/react.js +214 -0
  36. package/dist/core/projects/createBankFlow.js +252 -0
  37. package/dist/core/projects/createBankIterationPrompt.js +294 -0
  38. package/dist/core/projects/createBankPrompt.js +95 -0
  39. package/dist/core/projects/createFlowPhases.js +28 -0
  40. package/dist/core/projects/discoverCurrentProjectBank.js +43 -0
  41. package/dist/core/projects/discoverExistingGuidance.js +99 -0
  42. package/dist/core/projects/discoverProjectEvidence.js +87 -0
  43. package/dist/core/projects/discoverRecentCommits.js +28 -0
  44. package/dist/core/projects/findReferenceProjects.js +42 -0
  45. package/dist/core/projects/guidanceStrategies.js +29 -0
  46. package/dist/core/projects/identity.js +16 -0
  47. package/dist/core/projects/providerProjectGuidance.js +82 -0
  48. package/dist/core/providers/providerRegistry.js +51 -0
  49. package/dist/core/providers/types.js +1 -0
  50. package/dist/core/stats/statsService.js +117 -0
  51. package/dist/core/sync/syncService.js +145 -0
  52. package/dist/core/sync/syncTypes.js +1 -0
  53. package/dist/core/upgrade/upgradeService.js +134 -0
  54. package/dist/integrations/claudeCode/install.js +78 -0
  55. package/dist/integrations/codex/install.js +80 -0
  56. package/dist/integrations/commandRunner.js +32 -0
  57. package/dist/integrations/cursor/install.js +118 -0
  58. package/dist/integrations/shared.js +20 -0
  59. package/dist/mcp/config.js +19 -0
  60. package/dist/mcp/createMcpServer.js +31 -0
  61. package/dist/mcp/launcher.js +49 -0
  62. package/dist/mcp/registerTools.js +33 -0
  63. package/dist/mcp/serverMetadata.js +7 -0
  64. package/dist/mcp/tools/auditUtils.js +49 -0
  65. package/dist/mcp/tools/createBankApply.js +106 -0
  66. package/dist/mcp/tools/createBankToolRuntime.js +115 -0
  67. package/dist/mcp/tools/createBankToolSchemas.js +234 -0
  68. package/dist/mcp/tools/entryMutationHelpers.js +44 -0
  69. package/dist/mcp/tools/registerBankManifestTool.js +47 -0
  70. package/dist/mcp/tools/registerClearProjectBankTool.js +73 -0
  71. package/dist/mcp/tools/registerCreateBankTool.js +240 -0
  72. package/dist/mcp/tools/registerDeleteEntryTool.js +98 -0
  73. package/dist/mcp/tools/registerDeleteGuidanceSourceTool.js +120 -0
  74. package/dist/mcp/tools/registerListEntriesTool.js +94 -0
  75. package/dist/mcp/tools/registerReadEntryTool.js +99 -0
  76. package/dist/mcp/tools/registerResolveContextTool.js +128 -0
  77. package/dist/mcp/tools/registerSetProjectStateTool.js +121 -0
  78. package/dist/mcp/tools/registerSyncBankTool.js +113 -0
  79. package/dist/mcp/tools/registerUpgradeBankTool.js +89 -0
  80. package/dist/mcp/tools/registerUpsertRuleTool.js +100 -0
  81. package/dist/mcp/tools/registerUpsertSkillTool.js +102 -0
  82. package/dist/mcp/tools/sharedSchemas.js +13 -0
  83. package/dist/shared/errors.js +18 -0
  84. package/dist/shared/paths.js +11 -0
  85. package/dist/storage/atomicWrite.js +15 -0
  86. package/dist/storage/auditLogger.js +20 -0
  87. package/dist/storage/auditStore.js +22 -0
  88. package/dist/storage/bankRepository.js +168 -0
  89. package/dist/storage/entryStore.js +142 -0
  90. package/dist/storage/manifestStore.js +30 -0
  91. package/dist/storage/projectBankStore.js +55 -0
  92. package/dist/storage/providerIntegrationStore.js +22 -0
  93. package/dist/storage/safeFs.js +202 -0
  94. package/package.json +64 -0
  95. package/scripts/postinstall.js +20 -0
@@ -0,0 +1,42 @@
1
+ import { detectProjectContext } from "../context/detectProjectContext.js";
2
+ import { DETECTABLE_STACKS } from "../context/types.js";
3
+ const intersectStacks = (left, right) => left.filter((stack) => right.includes(stack));
4
+ const detectableStackSet = new Set(DETECTABLE_STACKS);
5
+ export const findReferenceProjects = async ({ repository, currentProjectId, detectedStacks, }) => {
6
+ const manifests = await repository.listProjectManifests();
7
+ const candidates = await Promise.all(manifests
8
+ .filter((manifest) => manifest.projectId !== currentProjectId)
9
+ .map(async (manifest) => {
10
+ const manifestStacks = manifest.detectedStacks.length > 0
11
+ ? manifest.detectedStacks
12
+ : (await detectProjectContext(manifest.projectPath).catch(() => null))?.detectedStacks ?? [];
13
+ const normalizedStacks = manifestStacks.filter((stack) => detectableStackSet.has(stack));
14
+ const sharedStacks = intersectStacks(detectedStacks, normalizedStacks);
15
+ return {
16
+ projectId: manifest.projectId,
17
+ projectName: manifest.projectName,
18
+ projectPath: manifest.projectPath,
19
+ projectBankPath: repository.paths.projectDirectory(manifest.projectId),
20
+ detectedStacks: normalizedStacks,
21
+ sharedStacks,
22
+ updatedAt: manifest.updatedAt,
23
+ };
24
+ }));
25
+ return candidates
26
+ .filter((candidate) => candidate.sharedStacks.length > 0)
27
+ .sort((left, right) => {
28
+ if (right.sharedStacks.length !== left.sharedStacks.length) {
29
+ return right.sharedStacks.length - left.sharedStacks.length;
30
+ }
31
+ return right.updatedAt.localeCompare(left.updatedAt);
32
+ })
33
+ .slice(0, 5)
34
+ .map((candidate) => ({
35
+ projectId: candidate.projectId,
36
+ projectName: candidate.projectName,
37
+ projectPath: candidate.projectPath,
38
+ projectBankPath: candidate.projectBankPath,
39
+ detectedStacks: candidate.detectedStacks,
40
+ sharedStacks: candidate.sharedStacks,
41
+ }));
42
+ };
@@ -0,0 +1,29 @@
1
+ export const GUIDANCE_SOURCE_STRATEGIES = ["ignore", "copy", "move", "keep_source_fill_gaps"];
2
+ export const SOURCE_REVIEW_DECISIONS = ["ok", "not_ok"];
3
+ export const formatGuidanceSourceStrategy = (strategy) => {
4
+ switch (strategy) {
5
+ case "ignore":
6
+ return "ignore";
7
+ case "copy":
8
+ return "copy";
9
+ case "move":
10
+ return "move";
11
+ case "keep_source_fill_gaps":
12
+ return "keep source, fill gaps in bank";
13
+ }
14
+ };
15
+ export const buildDefaultSourceStrategies = (sources, decision) => {
16
+ return sources.map((source) => ({
17
+ sourceRef: source.relativePath,
18
+ strategy: source.entryType === "directory"
19
+ ? "ignore"
20
+ : decision === "ok"
21
+ ? "move"
22
+ : "copy",
23
+ note: source.entryType === "directory"
24
+ ? "Handled as a container while file-level guidance is migrated."
25
+ : decision === "ok"
26
+ ? "Confirmed by the default canonicalization flow with legacy cleanup allowed."
27
+ : "Confirmed by the default canonicalization flow while keeping legacy guidance in place.",
28
+ }));
29
+ };
@@ -0,0 +1,16 @@
1
+ import { createHash } from "node:crypto";
2
+ import path from "node:path";
3
+ const normalizeProjectPath = (projectPath) => {
4
+ const resolvedPath = path.resolve(projectPath);
5
+ return resolvedPath.endsWith(path.sep) ? resolvedPath.slice(0, -1) : resolvedPath;
6
+ };
7
+ export const resolveProjectIdentity = (projectPath) => {
8
+ const normalizedProjectPath = normalizeProjectPath(projectPath);
9
+ const projectName = path.basename(normalizedProjectPath);
10
+ const hash = createHash("sha256").update(normalizedProjectPath).digest("hex").slice(0, 12);
11
+ return {
12
+ projectId: `${projectName}-${hash}`,
13
+ projectName,
14
+ projectPath: normalizedProjectPath,
15
+ };
16
+ };
@@ -0,0 +1,82 @@
1
+ import os from "node:os";
2
+ import path from "node:path";
3
+ import { promises as fs } from "node:fs";
4
+ const pathExists = async (targetPath) => {
5
+ try {
6
+ await fs.access(targetPath);
7
+ return true;
8
+ }
9
+ catch {
10
+ return false;
11
+ }
12
+ };
13
+ const getHomePath = () => process.env.HOME ?? os.homedir();
14
+ const listFilesRecursively = async (directoryPath) => {
15
+ const directoryEntries = await fs.readdir(directoryPath, { withFileTypes: true });
16
+ const filePaths = [];
17
+ for (const directoryEntry of directoryEntries.sort((left, right) => left.name.localeCompare(right.name))) {
18
+ const entryPath = path.join(directoryPath, directoryEntry.name);
19
+ if (directoryEntry.isDirectory()) {
20
+ filePaths.push(...(await listFilesRecursively(entryPath)));
21
+ continue;
22
+ }
23
+ if (directoryEntry.isFile()) {
24
+ filePaths.push(entryPath);
25
+ }
26
+ }
27
+ return filePaths;
28
+ };
29
+ const toHomeRelativePath = (targetPath) => {
30
+ const homePath = getHomePath();
31
+ const relativeToHome = path.relative(homePath, targetPath);
32
+ return relativeToHome.startsWith("..") || path.isAbsolute(relativeToHome) ? targetPath : `~/${relativeToHome}`;
33
+ };
34
+ const encodeProjectPathForCursor = (projectPath) => path
35
+ .resolve(projectPath)
36
+ .split(path.sep)
37
+ .filter(Boolean)
38
+ .join("-")
39
+ .replaceAll(" ", "-");
40
+ const encodeProjectPathForClaude = (projectPath) => `-${encodeProjectPathForCursor(projectPath)}`;
41
+ const getCandidateRoots = (projectPath) => {
42
+ const projectName = path.basename(path.resolve(projectPath));
43
+ const homePath = getHomePath();
44
+ return [
45
+ {
46
+ provider: "codex",
47
+ rootPath: path.join(homePath, ".codex", "skills", "projects", projectName),
48
+ },
49
+ {
50
+ provider: "cursor",
51
+ rootPath: path.join(homePath, ".cursor", "projects", encodeProjectPathForCursor(projectPath), "rules"),
52
+ },
53
+ {
54
+ provider: "claude",
55
+ rootPath: path.join(homePath, ".claude", "projects", encodeProjectPathForClaude(projectPath), "skills"),
56
+ },
57
+ ];
58
+ };
59
+ export const discoverProviderProjectGuidance = async (projectPath) => {
60
+ const discoveredSources = [];
61
+ for (const candidate of getCandidateRoots(projectPath)) {
62
+ if (!(await pathExists(candidate.rootPath))) {
63
+ continue;
64
+ }
65
+ discoveredSources.push({
66
+ provider: candidate.provider,
67
+ entryType: "directory",
68
+ path: candidate.rootPath,
69
+ relativePath: toHomeRelativePath(candidate.rootPath),
70
+ });
71
+ const nestedFilePaths = await listFilesRecursively(candidate.rootPath);
72
+ for (const nestedFilePath of nestedFilePaths) {
73
+ discoveredSources.push({
74
+ provider: candidate.provider,
75
+ entryType: "file",
76
+ path: nestedFilePath,
77
+ relativePath: toHomeRelativePath(nestedFilePath),
78
+ });
79
+ }
80
+ }
81
+ return discoveredSources.sort((left, right) => left.relativePath.localeCompare(right.relativePath));
82
+ };
@@ -0,0 +1,51 @@
1
+ import { access } from "node:fs/promises";
2
+ import os from "node:os";
3
+ import path from "node:path";
4
+ import { installClaudeCodeIntegration } from "../../integrations/claudeCode/install.js";
5
+ import { installCodexIntegration } from "../../integrations/codex/install.js";
6
+ import { installCursorIntegration } from "../../integrations/cursor/install.js";
7
+ import { PROVIDER_IDS } from "../bank/types.js";
8
+ const pathExists = async (targetPath) => {
9
+ try {
10
+ await access(targetPath);
11
+ return true;
12
+ }
13
+ catch {
14
+ return false;
15
+ }
16
+ };
17
+ const isCursorEnvironmentAvailable = async () => (await pathExists(path.join(os.homedir(), ".cursor"))) ||
18
+ (await pathExists(path.join(os.homedir(), "Library", "Application Support", "Cursor")));
19
+ export const PROVIDER_DEFINITIONS = [
20
+ {
21
+ id: "codex",
22
+ displayName: "Codex",
23
+ cliCommand: "codex",
24
+ unavailableMessage: "Codex CLI was not found on PATH.",
25
+ install: installCodexIntegration,
26
+ },
27
+ {
28
+ id: "cursor",
29
+ displayName: "Cursor",
30
+ cliCommand: "cursor",
31
+ unavailableMessage: "Cursor local configuration directories were not found.",
32
+ isAvailable: isCursorEnvironmentAvailable,
33
+ install: installCursorIntegration,
34
+ },
35
+ {
36
+ id: "claude-code",
37
+ displayName: "Claude Code",
38
+ cliCommand: "claude",
39
+ unavailableMessage: "Claude Code CLI was not found on PATH.",
40
+ install: installClaudeCodeIntegration,
41
+ },
42
+ ];
43
+ const providerMap = new Map(PROVIDER_DEFINITIONS.map((definition) => [definition.id, definition]));
44
+ export const getProviderDefinition = (providerId) => {
45
+ const definition = providerMap.get(providerId);
46
+ if (!definition) {
47
+ throw new Error(`Unsupported provider: ${providerId}`);
48
+ }
49
+ return definition;
50
+ };
51
+ export const isProviderId = (value) => PROVIDER_IDS.includes(value);
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,117 @@
1
+ import path from "node:path";
2
+ import { BankRepository } from "../../storage/bankRepository.js";
3
+ import { resolveBankRoot } from "../../shared/paths.js";
4
+ const incrementCount = (bucket, key) => {
5
+ bucket[key] = (bucket[key] ?? 0) + 1;
6
+ };
7
+ const isDocumentationEntry = (entryPath) => {
8
+ const normalizedEntryPath = entryPath.replaceAll("\\", "/").toLowerCase();
9
+ return normalizedEntryPath.endsWith("/readme.md") || normalizedEntryPath === "readme.md";
10
+ };
11
+ const summarizeEvents = (events, limit) => [...events]
12
+ .sort((left, right) => right.timestamp.localeCompare(left.timestamp))
13
+ .slice(0, limit)
14
+ .map((event) => ({
15
+ timestamp: event.timestamp,
16
+ provider: event.provider,
17
+ tool: event.tool,
18
+ action: event.action,
19
+ projectId: event.projectId,
20
+ projectPath: event.projectPath,
21
+ sessionRef: event.sessionRef,
22
+ }));
23
+ const summarizeEventsByKey = (events, selector) => {
24
+ const counts = {};
25
+ for (const event of events) {
26
+ incrementCount(counts, selector(event));
27
+ }
28
+ return counts;
29
+ };
30
+ const countEntries = async (repository, scope, projectId) => {
31
+ const rules = await repository.listLayerEntries(scope, "rules", projectId);
32
+ const skills = await repository.listLayerEntries(scope, "skills", projectId);
33
+ return {
34
+ rules: rules.filter((entry) => !isDocumentationEntry(entry.path)).length,
35
+ skills: skills.filter((entry) => !isDocumentationEntry(entry.path)).length,
36
+ };
37
+ };
38
+ const resolveProjectFilter = (manifests, projectPath) => {
39
+ if (!projectPath) {
40
+ return null;
41
+ }
42
+ const resolvedProjectPath = path.resolve(projectPath);
43
+ return manifests.find((manifest) => path.resolve(manifest.projectPath) === resolvedProjectPath) ?? null;
44
+ };
45
+ export class StatsService {
46
+ repository;
47
+ constructor(bankRoot) {
48
+ this.repository = new BankRepository(resolveBankRoot(bankRoot));
49
+ }
50
+ async collect(options) {
51
+ const latestEventsLimit = options?.latestEventsLimit ?? 10;
52
+ const manifest = await this.repository.readManifestOptional();
53
+ if (manifest === null) {
54
+ throw new Error(`AI Guidance Bank is not initialized at ${this.repository.rootPath}. Run \`gbank init\` first.`);
55
+ }
56
+ const projectManifests = await this.repository.listProjectManifests();
57
+ const auditEvents = await this.repository.readAuditEventsOptional();
58
+ const sharedEntries = await countEntries(this.repository, "shared");
59
+ const projectCountsByState = {};
60
+ for (const projectManifest of projectManifests) {
61
+ const state = await this.repository.readProjectStateOptional(projectManifest.projectId);
62
+ incrementCount(projectCountsByState, state?.creationState ?? "unknown");
63
+ }
64
+ const matchedProject = resolveProjectFilter(projectManifests, options?.projectPath);
65
+ let projectStats;
66
+ if (options?.projectPath) {
67
+ if (matchedProject === null) {
68
+ throw new Error(`No project bank found for ${path.resolve(options.projectPath)}.`);
69
+ }
70
+ const projectState = await this.repository.readProjectStateOptional(matchedProject.projectId);
71
+ const projectEntries = await countEntries(this.repository, "project", matchedProject.projectId);
72
+ const projectEvents = auditEvents.filter((event) => event.projectId === matchedProject.projectId);
73
+ projectStats = {
74
+ projectId: matchedProject.projectId,
75
+ projectName: matchedProject.projectName,
76
+ projectPath: matchedProject.projectPath,
77
+ detectedStacks: matchedProject.detectedStacks,
78
+ creationState: projectState?.creationState ?? "unknown",
79
+ updatedAt: projectState?.updatedAt ?? matchedProject.updatedAt,
80
+ entries: projectEntries,
81
+ audit: {
82
+ totalEvents: projectEvents.length,
83
+ latestEvents: summarizeEvents(projectEvents, latestEventsLimit),
84
+ byTool: summarizeEventsByKey(projectEvents, (event) => event.tool),
85
+ byProvider: summarizeEventsByKey(projectEvents, (event) => event.provider ?? "unknown"),
86
+ },
87
+ };
88
+ }
89
+ return {
90
+ bankRoot: this.repository.rootPath,
91
+ manifest: {
92
+ bankId: manifest.bankId,
93
+ storageVersion: manifest.storageVersion,
94
+ createdAt: manifest.createdAt,
95
+ updatedAt: manifest.updatedAt,
96
+ enabledProviders: manifest.enabledProviders,
97
+ defaultMcpTransport: manifest.defaultMcpTransport,
98
+ },
99
+ sharedEntries,
100
+ projects: {
101
+ total: projectManifests.length,
102
+ byCreationState: projectCountsByState,
103
+ },
104
+ audit: {
105
+ totalEvents: auditEvents.length,
106
+ byTool: summarizeEventsByKey(auditEvents, (event) => event.tool),
107
+ byProvider: summarizeEventsByKey(auditEvents, (event) => event.provider ?? "unknown"),
108
+ latestEvents: summarizeEvents(auditEvents, latestEventsLimit),
109
+ },
110
+ ...(projectStats
111
+ ? {
112
+ project: projectStats,
113
+ }
114
+ : {}),
115
+ };
116
+ }
117
+ }
@@ -0,0 +1,145 @@
1
+ import { parseCanonicalRuleDocument, parseCanonicalSkillDocument } from "../bank/canonicalEntry.js";
2
+ import { createProjectBankState, markProjectBankSynced, postponeProjectBankSync, updateProjectBankManifest, } from "../bank/project.js";
3
+ import { detectProjectContext } from "../context/detectProjectContext.js";
4
+ import { resolveProjectIdentity } from "../projects/identity.js";
5
+ import { BankRepository } from "../../storage/bankRepository.js";
6
+ import { ValidationError } from "../../shared/errors.js";
7
+ import { resolveBankRoot } from "../../shared/paths.js";
8
+ const isDocumentationFile = (entryPath) => {
9
+ const normalizedEntryPath = entryPath.replaceAll("\\", "/").toLowerCase();
10
+ return normalizedEntryPath.endsWith("/readme.md") || normalizedEntryPath === "readme.md";
11
+ };
12
+ const validateLayer = async (repository, layer, kind, projectId) => {
13
+ const entries = await repository.listLayerEntries(layer, kind, projectId);
14
+ let validatedCount = 0;
15
+ for (const entry of entries) {
16
+ if (isDocumentationFile(entry.path)) {
17
+ continue;
18
+ }
19
+ const content = await repository.readLayerEntry(layer, kind, entry.path, projectId);
20
+ try {
21
+ if (kind === "rules") {
22
+ parseCanonicalRuleDocument(content);
23
+ }
24
+ else {
25
+ parseCanonicalSkillDocument(content);
26
+ }
27
+ }
28
+ catch (error) {
29
+ const message = error instanceof Error ? error.message : "Unknown canonical entry parsing error.";
30
+ throw new ValidationError(`Invalid canonical ${kind.slice(0, -1)} at ${layer}/${entry.path}: ${message}`);
31
+ }
32
+ validatedCount += 1;
33
+ }
34
+ return validatedCount;
35
+ };
36
+ export class SyncService {
37
+ async run(options) {
38
+ const bankRoot = resolveBankRoot(options.bankRoot);
39
+ const repository = new BankRepository(bankRoot);
40
+ const manifest = await repository.readManifestOptional();
41
+ if (manifest === null) {
42
+ throw new ValidationError("AI Guidance Bank is not initialized yet. Run gbank init first.");
43
+ }
44
+ const identity = resolveProjectIdentity(options.projectPath);
45
+ const projectContext = await detectProjectContext(identity.projectPath);
46
+ const projectManifest = await repository.readProjectManifestOptional(identity.projectId);
47
+ const projectState = await repository.readProjectStateOptional(identity.projectId);
48
+ const sharedRules = await validateLayer(repository, "shared", "rules");
49
+ const sharedSkills = await validateLayer(repository, "shared", "skills");
50
+ const projectRules = projectManifest !== null ? await validateLayer(repository, "project", "rules", identity.projectId) : 0;
51
+ const projectSkills = projectManifest !== null ? await validateLayer(repository, "project", "skills", identity.projectId) : 0;
52
+ let projectManifestUpdated = false;
53
+ if (projectManifest !== null) {
54
+ const nextDetectedStacks = [...projectContext.detectedStacks];
55
+ const currentDetectedStacks = [...projectManifest.detectedStacks];
56
+ const stacksChanged = nextDetectedStacks.length !== currentDetectedStacks.length ||
57
+ nextDetectedStacks.some((stack, index) => stack !== currentDetectedStacks[index]);
58
+ if (stacksChanged) {
59
+ await repository.writeProjectManifest(identity.projectId, updateProjectBankManifest(projectManifest, projectContext.detectedStacks));
60
+ projectManifestUpdated = true;
61
+ }
62
+ }
63
+ if (projectManifest !== null) {
64
+ const nextProjectState = markProjectBankSynced(projectState ?? createProjectBankState("ready"), manifest.storageVersion);
65
+ await repository.writeProjectState(identity.projectId, nextProjectState);
66
+ return {
67
+ action: "run",
68
+ bankRoot,
69
+ projectPath: identity.projectPath,
70
+ detectedStacks: projectContext.detectedStacks,
71
+ projectState: nextProjectState.creationState,
72
+ postponedUntil: nextProjectState.postponedUntil,
73
+ projectManifestUpdated,
74
+ validatedEntries: {
75
+ shared: {
76
+ rules: sharedRules,
77
+ skills: sharedSkills,
78
+ },
79
+ project: {
80
+ rules: projectRules,
81
+ skills: projectSkills,
82
+ },
83
+ },
84
+ externalGuidanceSources: projectContext.localGuidance,
85
+ };
86
+ }
87
+ return {
88
+ action: "run",
89
+ bankRoot,
90
+ projectPath: identity.projectPath,
91
+ detectedStacks: projectContext.detectedStacks,
92
+ projectState: projectState?.creationState ?? "unknown",
93
+ postponedUntil: projectState?.postponedUntil ?? null,
94
+ projectManifestUpdated,
95
+ validatedEntries: {
96
+ shared: {
97
+ rules: sharedRules,
98
+ skills: sharedSkills,
99
+ },
100
+ project: {
101
+ rules: projectRules,
102
+ skills: projectSkills,
103
+ },
104
+ },
105
+ externalGuidanceSources: projectContext.localGuidance,
106
+ };
107
+ }
108
+ async postpone(options) {
109
+ const bankRoot = resolveBankRoot(options.bankRoot);
110
+ const repository = new BankRepository(bankRoot);
111
+ const manifest = await repository.readManifestOptional();
112
+ if (manifest === null) {
113
+ throw new ValidationError("AI Guidance Bank is not initialized yet. Run gbank init first.");
114
+ }
115
+ const identity = resolveProjectIdentity(options.projectPath);
116
+ const projectContext = await detectProjectContext(identity.projectPath);
117
+ const projectManifest = await repository.readProjectManifestOptional(identity.projectId);
118
+ if (projectManifest === null) {
119
+ throw new ValidationError("Project AI Guidance Bank does not exist yet. Call create_bank before postponing sync.");
120
+ }
121
+ const projectState = await repository.readProjectStateOptional(identity.projectId);
122
+ const nextProjectState = postponeProjectBankSync(projectState ?? createProjectBankState("ready"), 1);
123
+ await repository.writeProjectState(identity.projectId, nextProjectState);
124
+ return {
125
+ action: "postpone",
126
+ bankRoot,
127
+ projectPath: identity.projectPath,
128
+ detectedStacks: projectContext.detectedStacks,
129
+ projectState: nextProjectState.creationState,
130
+ postponedUntil: nextProjectState.postponedUntil,
131
+ projectManifestUpdated: false,
132
+ validatedEntries: {
133
+ shared: {
134
+ rules: 0,
135
+ skills: 0,
136
+ },
137
+ project: {
138
+ rules: 0,
139
+ skills: 0,
140
+ },
141
+ },
142
+ externalGuidanceSources: projectContext.localGuidance,
143
+ };
144
+ }
145
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,134 @@
1
+ import path from "node:path";
2
+ import { promises as fs } from "node:fs";
3
+ import { InitService } from "../init/initService.js";
4
+ import { BankRepository } from "../../storage/bankRepository.js";
5
+ import { ValidationError } from "../../shared/errors.js";
6
+ import { BANK_DIRECTORY_NAME, LEGACY_BANK_DIRECTORY_NAMES, resolveBankRoot } from "../../shared/paths.js";
7
+ import { CURRENT_STORAGE_VERSION } from "../bank/types.js";
8
+ import { isCurrentStorageVersion, updateManifest } from "../bank/manifest.js";
9
+ const resolveLegacyBankRoots = (bankRoot) => {
10
+ const resolvedBankRoot = path.resolve(bankRoot);
11
+ if (path.basename(resolvedBankRoot) !== BANK_DIRECTORY_NAME) {
12
+ return [];
13
+ }
14
+ return LEGACY_BANK_DIRECTORY_NAMES.map((directoryName) => path.join(path.dirname(resolvedBankRoot), directoryName));
15
+ };
16
+ const moveBankRoot = async (sourceRoot, targetRoot) => {
17
+ if (sourceRoot === targetRoot) {
18
+ return;
19
+ }
20
+ try {
21
+ await fs.access(targetRoot);
22
+ throw new ValidationError(`Cannot upgrade AI Guidance Bank into an existing path: ${targetRoot}`);
23
+ }
24
+ catch (error) {
25
+ if (error.code !== "ENOENT") {
26
+ throw error;
27
+ }
28
+ }
29
+ await fs.mkdir(path.dirname(targetRoot), { recursive: true });
30
+ try {
31
+ await fs.rename(sourceRoot, targetRoot);
32
+ }
33
+ catch (error) {
34
+ if (error.code !== "EXDEV") {
35
+ throw error;
36
+ }
37
+ await fs.cp(sourceRoot, targetRoot, { recursive: true });
38
+ await fs.rm(sourceRoot, { recursive: true, force: true });
39
+ }
40
+ };
41
+ export const detectBankUpgrade = async (bankRoot) => {
42
+ const resolvedBankRoot = resolveBankRoot(bankRoot);
43
+ const repository = new BankRepository(resolvedBankRoot);
44
+ const manifest = await repository.readManifestOptional();
45
+ if (manifest !== null) {
46
+ if (isCurrentStorageVersion(manifest.storageVersion)) {
47
+ return {
48
+ status: "up_to_date",
49
+ bankRoot: resolvedBankRoot,
50
+ manifest,
51
+ expectedStorageVersion: CURRENT_STORAGE_VERSION,
52
+ };
53
+ }
54
+ return {
55
+ status: "upgrade_required",
56
+ bankRoot: resolvedBankRoot,
57
+ sourceRoot: resolvedBankRoot,
58
+ manifest,
59
+ expectedStorageVersion: CURRENT_STORAGE_VERSION,
60
+ reason: "storage_version",
61
+ };
62
+ }
63
+ const legacyMatches = [];
64
+ for (const legacyBankRoot of resolveLegacyBankRoots(resolvedBankRoot)) {
65
+ const legacyRepository = new BankRepository(legacyBankRoot);
66
+ const legacyManifest = await legacyRepository.readManifestOptional();
67
+ if (legacyManifest !== null) {
68
+ legacyMatches.push({ legacyBankRoot, manifest: legacyManifest });
69
+ }
70
+ }
71
+ if (legacyMatches.length > 1) {
72
+ throw new ValidationError(`Multiple legacy AI Guidance Bank roots were found: ${legacyMatches.map(({ legacyBankRoot }) => legacyBankRoot).join(", ")}. Resolve them manually before upgrading.`);
73
+ }
74
+ if (legacyMatches.length === 1) {
75
+ const legacyMatch = legacyMatches[0];
76
+ return {
77
+ status: "upgrade_required",
78
+ bankRoot: resolvedBankRoot,
79
+ sourceRoot: legacyMatch.legacyBankRoot,
80
+ manifest: legacyMatch.manifest,
81
+ expectedStorageVersion: CURRENT_STORAGE_VERSION,
82
+ reason: "legacy_root",
83
+ };
84
+ }
85
+ return {
86
+ status: "not_initialized",
87
+ bankRoot: resolvedBankRoot,
88
+ expectedStorageVersion: CURRENT_STORAGE_VERSION,
89
+ };
90
+ };
91
+ export class UpgradeService {
92
+ async run(options) {
93
+ const detection = await detectBankUpgrade(options?.bankRoot);
94
+ if (detection.status === "not_initialized") {
95
+ throw new ValidationError(`AI Guidance Bank is not initialized yet. Run \`gbank init\` first.`);
96
+ }
97
+ if (detection.status === "up_to_date") {
98
+ return {
99
+ status: "already_current",
100
+ bankRoot: detection.bankRoot,
101
+ sourceRoot: detection.bankRoot,
102
+ migratedBankRoot: false,
103
+ previousStorageVersion: detection.manifest.storageVersion,
104
+ storageVersion: detection.manifest.storageVersion,
105
+ enabledProviders: detection.manifest.enabledProviders,
106
+ };
107
+ }
108
+ if (detection.reason === "legacy_root") {
109
+ await moveBankRoot(detection.sourceRoot, detection.bankRoot);
110
+ }
111
+ const repository = new BankRepository(detection.bankRoot);
112
+ const manifestBeforeUpgrade = await repository.readManifest();
113
+ const initService = new InitService();
114
+ await initService.run({
115
+ bankRoot: detection.bankRoot,
116
+ selectedProviders: manifestBeforeUpgrade.enabledProviders,
117
+ ...(options?.cursorConfigRoot ? { cursorConfigRoot: options.cursorConfigRoot } : {}),
118
+ ...(options?.commandRunner ? { commandRunner: options.commandRunner } : {}),
119
+ });
120
+ await repository.writeManifest(updateManifest(manifestBeforeUpgrade, manifestBeforeUpgrade.enabledProviders, new Date(), {
121
+ storageVersion: CURRENT_STORAGE_VERSION,
122
+ }));
123
+ const upgradedManifest = await repository.readManifest();
124
+ return {
125
+ status: "upgraded",
126
+ bankRoot: detection.bankRoot,
127
+ sourceRoot: detection.sourceRoot,
128
+ migratedBankRoot: detection.sourceRoot !== detection.bankRoot,
129
+ previousStorageVersion: detection.manifest.storageVersion,
130
+ storageVersion: upgradedManifest.storageVersion,
131
+ enabledProviders: upgradedManifest.enabledProviders,
132
+ };
133
+ }
134
+ }