@aikotools/repo-maintenance 1.0.2

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 (29) hide show
  1. package/README.md +227 -0
  2. package/bin/loader.js +10 -0
  3. package/bin/repohub.js +5 -0
  4. package/dist/client/assets/index-Bh_JYZxI.js +15 -0
  5. package/dist/client/assets/index-D-CxJxP4.css +1 -0
  6. package/dist/client/index.html +13 -0
  7. package/dist/server/server/index.js +118 -0
  8. package/dist/server/server/services/bulk-service.js +157 -0
  9. package/dist/server/server/services/cascade-service.js +474 -0
  10. package/dist/server/server/services/config-service.js +343 -0
  11. package/dist/server/server/services/dependency-resolver.js +144 -0
  12. package/dist/server/server/services/git-service.js +320 -0
  13. package/dist/server/server/services/package-service.js +152 -0
  14. package/dist/server/server/services/process.js +51 -0
  15. package/dist/server/server/services/pull-all-service.js +415 -0
  16. package/dist/server/server/services/repo-scanner.js +230 -0
  17. package/dist/server/server/services/task-queue.js +29 -0
  18. package/dist/server/server/trpc/context.js +4 -0
  19. package/dist/server/server/trpc/init.js +7 -0
  20. package/dist/server/server/trpc/procedures/bulk.js +110 -0
  21. package/dist/server/server/trpc/procedures/cascade.js +207 -0
  22. package/dist/server/server/trpc/procedures/dependencies.js +26 -0
  23. package/dist/server/server/trpc/procedures/git.js +151 -0
  24. package/dist/server/server/trpc/procedures/package.js +43 -0
  25. package/dist/server/server/trpc/procedures/project.js +181 -0
  26. package/dist/server/server/trpc/procedures/repos.js +42 -0
  27. package/dist/server/server/trpc/router.js +20 -0
  28. package/dist/server/shared/types.js +3 -0
  29. package/package.json +68 -0
@@ -0,0 +1,181 @@
1
+ /**
2
+ * Project configuration procedures.
3
+ */
4
+ import { execSync } from 'child_process';
5
+ import { existsSync, readFileSync, statSync } from 'fs';
6
+ import path from 'path';
7
+ import { z } from 'zod';
8
+ import { BulkService } from '../../services/bulk-service';
9
+ import { CascadeService } from '../../services/cascade-service';
10
+ import { DependencyResolver } from '../../services/dependency-resolver';
11
+ import { GitService } from '../../services/git-service';
12
+ import { PackageService } from '../../services/package-service';
13
+ import { PullAllService } from '../../services/pull-all-service';
14
+ import { RepoScanner } from '../../services/repo-scanner';
15
+ import { publicProcedure, router } from '../init';
16
+ /** Re-initialize all services based on config values */
17
+ function reinitializeContext(ctx, config) {
18
+ const rootFolder = config.rootFolder || '';
19
+ ctx.scanner = new RepoScanner(rootFolder, config.npmOrganizations || []);
20
+ ctx.gitService = new GitService(config.parallelTasks || 6);
21
+ ctx.cascadeService = new CascadeService(ctx.configService, config.parallelTasks || 6);
22
+ ctx.bulkService = new BulkService(config.parallelTasks || 6);
23
+ ctx.pullAllService = new PullAllService(config.parallelTasks || 6, ctx.configService);
24
+ ctx.packageService = new PackageService(rootFolder, config.npmOrganizations || []);
25
+ ctx.repos = [];
26
+ ctx.domains = [];
27
+ ctx.dependencyResolver = null;
28
+ }
29
+ /** Load cached repos/domains/graph into context */
30
+ export async function loadCachedData(ctx) {
31
+ const cachedRepos = await ctx.configService.getCachedRepos();
32
+ if (cachedRepos && cachedRepos.length > 0) {
33
+ ctx.repos = cachedRepos;
34
+ ctx.dependencyResolver = new DependencyResolver(cachedRepos);
35
+ ctx.dependencyResolver.buildGraph();
36
+ // Rebuild domains from cached repos
37
+ const domainMap = new Map();
38
+ for (const repo of cachedRepos) {
39
+ if (!domainMap.has(repo.domain)) {
40
+ domainMap.set(repo.domain, {
41
+ id: repo.domain,
42
+ path: `${repo.domain}/`,
43
+ repoCount: 0,
44
+ hasUncommitted: false,
45
+ subGroups: [],
46
+ });
47
+ }
48
+ domainMap.get(repo.domain).repoCount++;
49
+ }
50
+ ctx.domains = Array.from(domainMap.values()).sort((a, b) => a.id.localeCompare(b.id));
51
+ }
52
+ else {
53
+ ctx.repos = [];
54
+ ctx.domains = [];
55
+ ctx.dependencyResolver = null;
56
+ }
57
+ }
58
+ export const projectRouter = router({
59
+ get: publicProcedure.query(async ({ ctx }) => {
60
+ return ctx.configService.getProjectConfig();
61
+ }),
62
+ update: publicProcedure
63
+ .input(z.object({
64
+ name: z.string().optional(),
65
+ rootFolder: z.string().optional(),
66
+ npmOrganizations: z.array(z.string()).optional(),
67
+ githubOrganizations: z.array(z.string()).optional(),
68
+ npmRegistry: z.string().optional(),
69
+ parallelTasks: z.number().min(1).max(20).optional(),
70
+ defaultBranch: z.string().optional(),
71
+ domainOverrides: z.record(z.string(), z.string()).optional(),
72
+ }))
73
+ .mutation(async ({ ctx, input }) => {
74
+ if (input.rootFolder) {
75
+ if (!existsSync(input.rootFolder) || !statSync(input.rootFolder).isDirectory()) {
76
+ throw new Error(`Folder does not exist: ${input.rootFolder}`);
77
+ }
78
+ }
79
+ const current = await ctx.configService.getProjectConfig();
80
+ const updated = { ...current, ...input };
81
+ await ctx.configService.saveProjectConfig(updated);
82
+ reinitializeContext(ctx, updated);
83
+ return updated;
84
+ }),
85
+ importMapping: publicProcedure
86
+ .input(z.object({ scriptPath: z.string().optional() }).optional())
87
+ .mutation(async ({ ctx, input }) => {
88
+ // Find repo-maintenance.sh: use provided path or auto-detect in rootFolder
89
+ const config = await ctx.configService.getProjectConfig();
90
+ const scriptPath = input?.scriptPath ||
91
+ path.join(config.rootFolder, 'repo-maintenance.sh');
92
+ if (!existsSync(scriptPath)) {
93
+ throw new Error(`Script not found: ${scriptPath}`);
94
+ }
95
+ const scriptContent = readFileSync(scriptPath, 'utf-8');
96
+ const result = await ctx.configService.importRepoMapping(scriptContent);
97
+ return {
98
+ mappingCount: Object.keys(result.mapping).length,
99
+ ignoreCount: result.ignore.length,
100
+ };
101
+ }),
102
+ /** Update the full repo mapping (replace all entries) */
103
+ updateRepoMapping: publicProcedure
104
+ .input(z.object({ repoMapping: z.record(z.string(), z.string()) }))
105
+ .mutation(async ({ ctx, input }) => {
106
+ const config = await ctx.configService.getProjectConfig();
107
+ config.repoMapping = input.repoMapping;
108
+ await ctx.configService.saveProjectConfig(config);
109
+ return { count: Object.keys(input.repoMapping).length };
110
+ }),
111
+ /** Update the full ignore list (replace all entries) */
112
+ updateIgnoreRepos: publicProcedure
113
+ .input(z.object({ ignoreRepos: z.array(z.string()) }))
114
+ .mutation(async ({ ctx, input }) => {
115
+ const config = await ctx.configService.getProjectConfig();
116
+ config.ignoreRepos = input.ignoreRepos;
117
+ await ctx.configService.saveProjectConfig(config);
118
+ return { count: input.ignoreRepos.length };
119
+ }),
120
+ browseFolder: publicProcedure
121
+ .input(z.object({ currentPath: z.string().optional() }).optional())
122
+ .mutation(({ input }) => {
123
+ const startDir = input?.currentPath || process.env.HOME || '/';
124
+ try {
125
+ const script = `
126
+ set defaultDir to POSIX file "${startDir}" as alias
127
+ set chosenFolder to choose folder with prompt "Select root folder" default location defaultDir
128
+ return POSIX path of chosenFolder
129
+ `;
130
+ const result = execSync(`osascript -e '${script}'`, {
131
+ timeout: 60_000,
132
+ encoding: 'utf-8',
133
+ }).trim();
134
+ return { path: result.replace(/\/$/, '') };
135
+ }
136
+ catch {
137
+ return { path: null };
138
+ }
139
+ }),
140
+ // ── Multi-project endpoints ──
141
+ listProjects: publicProcedure.query(async ({ ctx }) => {
142
+ return ctx.configService.listProjectSummaries();
143
+ }),
144
+ createProject: publicProcedure
145
+ .input(z.object({
146
+ name: z.string().min(1),
147
+ rootFolder: z.string(),
148
+ npmOrganizations: z.array(z.string()).optional(),
149
+ }))
150
+ .mutation(async ({ ctx, input }) => {
151
+ if (input.rootFolder && !existsSync(input.rootFolder)) {
152
+ throw new Error(`Folder does not exist: ${input.rootFolder}`);
153
+ }
154
+ const slug = await ctx.configService.createProject(input.name, input.rootFolder);
155
+ // If npm orgs provided, save them into the new project config
156
+ if (input.npmOrganizations?.length) {
157
+ const config = await ctx.configService.peekProjectConfig(slug);
158
+ config.npmOrganizations = input.npmOrganizations;
159
+ // We need to switch temporarily to save, then switch back
160
+ const currentSlug = ctx.configService.getActiveProjectSlug();
161
+ await ctx.configService.switchProject(slug);
162
+ await ctx.configService.saveProjectConfig(config);
163
+ await ctx.configService.switchProject(currentSlug);
164
+ }
165
+ return { slug };
166
+ }),
167
+ switchProject: publicProcedure
168
+ .input(z.object({ slug: z.string() }))
169
+ .mutation(async ({ ctx, input }) => {
170
+ const config = await ctx.configService.switchProject(input.slug);
171
+ reinitializeContext(ctx, config);
172
+ await loadCachedData(ctx);
173
+ return config;
174
+ }),
175
+ deleteProject: publicProcedure
176
+ .input(z.object({ slug: z.string() }))
177
+ .mutation(async ({ ctx, input }) => {
178
+ await ctx.configService.deleteProject(input.slug);
179
+ return { success: true };
180
+ }),
181
+ });
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Repository list, detail, and refresh procedures.
3
+ */
4
+ import { TRPCError } from '@trpc/server';
5
+ import { z } from 'zod';
6
+ import { DependencyResolver } from '../../services/dependency-resolver';
7
+ import { publicProcedure, router } from '../init';
8
+ export const reposRouter = router({
9
+ list: publicProcedure.query(({ ctx }) => {
10
+ return { repos: ctx.repos, domains: ctx.domains };
11
+ }),
12
+ refresh: publicProcedure.mutation(async ({ ctx }) => {
13
+ const config = await ctx.configService.getProjectConfig();
14
+ const { repos, domains } = await ctx.scanner.scan(config.domainOverrides);
15
+ const resolver = new DependencyResolver(repos);
16
+ const graph = resolver.buildGraph();
17
+ // Update in-memory state
18
+ ctx.repos = repos;
19
+ ctx.domains = domains;
20
+ ctx.dependencyResolver = resolver;
21
+ // Persist to cache
22
+ await ctx.configService.saveCachedRepos(repos);
23
+ await ctx.configService.saveCachedGraph(graph);
24
+ // Auto-build repoMapping from scanned directory structure
25
+ const repoMapping = {};
26
+ for (const repo of repos) {
27
+ const domainPath = repo.subGroup ? `${repo.domain}/${repo.subGroup}` : repo.domain;
28
+ repoMapping[repo.id] = domainPath;
29
+ }
30
+ config.repoMapping = repoMapping;
31
+ // Update lastRefresh timestamp
32
+ config.lastRefresh = new Date().toISOString();
33
+ await ctx.configService.saveProjectConfig(config);
34
+ return { repoCount: repos.length, domainCount: domains.length };
35
+ }),
36
+ detail: publicProcedure.input(z.object({ id: z.string() })).query(({ ctx, input }) => {
37
+ const repo = ctx.repos.find((r) => r.id === input.id);
38
+ if (!repo)
39
+ throw new TRPCError({ code: 'NOT_FOUND', message: `Repo ${input.id} not found` });
40
+ return repo;
41
+ }),
42
+ });
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Root tRPC router - combines all procedure routers.
3
+ */
4
+ import { router } from './init';
5
+ import { bulkRouter } from './procedures/bulk';
6
+ import { cascadeRouter } from './procedures/cascade';
7
+ import { dependenciesRouter } from './procedures/dependencies';
8
+ import { gitRouter } from './procedures/git';
9
+ import { packageRouter } from './procedures/package';
10
+ import { projectRouter } from './procedures/project';
11
+ import { reposRouter } from './procedures/repos';
12
+ export const appRouter = router({
13
+ project: projectRouter,
14
+ repos: reposRouter,
15
+ dependencies: dependenciesRouter,
16
+ git: gitRouter,
17
+ cascade: cascadeRouter,
18
+ bulk: bulkRouter,
19
+ package: packageRouter,
20
+ });
@@ -0,0 +1,3 @@
1
+ /**
2
+ * Shared types for RepoHub - used by both server and client.
3
+ */
package/package.json ADDED
@@ -0,0 +1,68 @@
1
+ {
2
+ "name": "@aikotools/repo-maintenance",
3
+ "version": "1.0.2",
4
+ "private": false,
5
+ "type": "module",
6
+ "publishConfig": {
7
+ "access": "public"
8
+ },
9
+ "bin": {
10
+ "repohub": "./bin/repohub.js"
11
+ },
12
+ "files": [
13
+ "dist/",
14
+ "bin/"
15
+ ],
16
+ "scripts": {
17
+ "dev": "concurrently \"pnpm run dev:server\" \"pnpm run dev:client\"",
18
+ "dev:server": "npx tsx --watch src/server/index.ts",
19
+ "dev:client": "vite",
20
+ "build": "tsc -p tsconfig.server.json && vite build",
21
+ "build:check": "tsc --noEmit",
22
+ "format": "prettier --write \"{src,tests}/**/*.{ts,tsx,css,json}\"",
23
+ "lint": "eslint \"{src,tests}/**/*.{ts,tsx}\"",
24
+ "test": "pnpm lint && pnpm build && pnpm depcheck && vitest --run --coverage",
25
+ "depcheck": "depcheck",
26
+ "start": "node bin/repohub.js"
27
+ },
28
+ "dependencies": {
29
+ "@hono/node-server": "1.13.8",
30
+ "@tanstack/react-query": "5.90.21",
31
+ "@trpc/client": "11.10.0",
32
+ "@trpc/react-query": "11.10.0",
33
+ "@trpc/server": "11.10.0",
34
+ "@xyflow/react": "12.10.0",
35
+ "hono": "4.11.9",
36
+ "lucide-react": "0.564.0",
37
+ "react": "19.2.4",
38
+ "react-dom": "19.2.4",
39
+ "simple-git": "3.27.0",
40
+ "zod": "4.3.6"
41
+ },
42
+ "devDependencies": {
43
+ "@eslint/js": "10.0.1",
44
+ "@semantic-release/commit-analyzer": "13.0.0",
45
+ "@semantic-release/github": "11.0.0",
46
+ "@semantic-release/npm": "12.0.0",
47
+ "@tailwindcss/postcss": "4.1.18",
48
+ "@types/node": "22.0.0",
49
+ "@types/react": "19.2.14",
50
+ "@types/react-dom": "19.2.3",
51
+ "@vitejs/plugin-react": "5.1.4",
52
+ "@vitest/coverage-v8": "4.0.18",
53
+ "concurrently": "9.0.0",
54
+ "depcheck": "1.4.7",
55
+ "eslint": "10.0.0",
56
+ "eslint-plugin-react-hooks": "7.0.1",
57
+ "eslint-plugin-react-refresh": "0.5.0",
58
+ "postcss": "8.5.6",
59
+ "prettier": "3.8.1",
60
+ "semantic-release": "24.0.0",
61
+ "tailwindcss": "4.1.18",
62
+ "tsx": "4.19.0",
63
+ "typescript": "5.9.3",
64
+ "typescript-eslint": "8.55.0",
65
+ "vite": "7.3.1",
66
+ "vitest": "4.0.18"
67
+ }
68
+ }