@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.
- package/README.md +227 -0
- package/bin/loader.js +10 -0
- package/bin/repohub.js +5 -0
- package/dist/client/assets/index-Bh_JYZxI.js +15 -0
- package/dist/client/assets/index-D-CxJxP4.css +1 -0
- package/dist/client/index.html +13 -0
- package/dist/server/server/index.js +118 -0
- package/dist/server/server/services/bulk-service.js +157 -0
- package/dist/server/server/services/cascade-service.js +474 -0
- package/dist/server/server/services/config-service.js +343 -0
- package/dist/server/server/services/dependency-resolver.js +144 -0
- package/dist/server/server/services/git-service.js +320 -0
- package/dist/server/server/services/package-service.js +152 -0
- package/dist/server/server/services/process.js +51 -0
- package/dist/server/server/services/pull-all-service.js +415 -0
- package/dist/server/server/services/repo-scanner.js +230 -0
- package/dist/server/server/services/task-queue.js +29 -0
- package/dist/server/server/trpc/context.js +4 -0
- package/dist/server/server/trpc/init.js +7 -0
- package/dist/server/server/trpc/procedures/bulk.js +110 -0
- package/dist/server/server/trpc/procedures/cascade.js +207 -0
- package/dist/server/server/trpc/procedures/dependencies.js +26 -0
- package/dist/server/server/trpc/procedures/git.js +151 -0
- package/dist/server/server/trpc/procedures/package.js +43 -0
- package/dist/server/server/trpc/procedures/project.js +181 -0
- package/dist/server/server/trpc/procedures/repos.js +42 -0
- package/dist/server/server/trpc/router.js +20 -0
- package/dist/server/shared/types.js +3 -0
- 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
|
+
});
|
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
|
+
}
|