gitforest 1.1.2 → 1.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.
@@ -0,0 +1,238 @@
1
+ import type { ViewMode } from "../../types/index.ts";
2
+ import type { CLIOptions } from "./list.ts";
3
+ import { showStatus } from "./list.ts";
4
+ import { scanAllDirectories, filterProjects } from "../../scanner/index.ts";
5
+ import { defaultGitHubService } from "../../services/github.ts";
6
+ import {
7
+ fetchUnifiedRepos,
8
+ filterByViewMode,
9
+ sortUnifiedRepos,
10
+ filterUnifiedRepos,
11
+ cloneGitHubRepo,
12
+ getUnifiedStats,
13
+ } from "../../github/unified.ts";
14
+ import { ensureAuthenticated } from "../../github/auth.ts";
15
+ import {
16
+ formatUnifiedRepoList,
17
+ formatWarning,
18
+ formatError,
19
+ formatInfo,
20
+ formatScanning,
21
+ formatOperationItem,
22
+ formatOperationSummary,
23
+ formatUnifiedStatusJson,
24
+ formatUnifiedStatusDisplay,
25
+ formatCloneItem,
26
+ } from "../formatters.ts";
27
+
28
+ /**
29
+ * Create GitHub repos for projects without remotes
30
+ */
31
+ export async function createGitHubRepos(options: CLIOptions & { isPrivate?: boolean }): Promise<void> {
32
+ const { config, filter, isPrivate = true } = options;
33
+
34
+ // Check GitHub auth - auto-login if not set
35
+ const token = await ensureAuthenticated();
36
+ if (!token) {
37
+ console.log(formatError("GitHub authentication required."));
38
+ console.log(formatInfo("Run 'gitforest login' to authenticate."));
39
+ process.exit(1);
40
+ }
41
+
42
+ console.log(formatScanning());
43
+ const projects = await scanAllDirectories(config);
44
+ let noRemoteProjects = projects.filter((p) => p.type === "git" && !p.status?.hasRemote);
45
+
46
+ if (filter) {
47
+ noRemoteProjects = filterProjects(noRemoteProjects, filter);
48
+ }
49
+
50
+ if (noRemoteProjects.length === 0) {
51
+ console.log(formatWarning("No projects without remotes found."));
52
+ return;
53
+ }
54
+
55
+ console.log(formatInfo(`\nCreating ${isPrivate ? "private" : "public"} repos for ${noRemoteProjects.length} projects...\n`));
56
+
57
+ let success = 0;
58
+ for (const project of noRemoteProjects) {
59
+ const result = await defaultGitHubService.createRepo({
60
+ name: project.name,
61
+ isPrivate,
62
+ localPath: project.path,
63
+ });
64
+
65
+ console.log(formatOperationItem(project.name, result.success, result.error));
66
+ if (result.success) success++;
67
+ }
68
+
69
+ console.log(`\n${formatOperationSummary("Created", success, noRemoteProjects.length)}`);
70
+ }
71
+
72
+ /**
73
+ * Archive GitHub repositories
74
+ */
75
+ export async function archiveRepos(options: CLIOptions & { repos: string[] }): Promise<void> {
76
+ const { repos } = options;
77
+
78
+ // Check GitHub auth - auto-login if not set
79
+ const token = await ensureAuthenticated();
80
+ if (!token) {
81
+ console.log(formatError("GitHub authentication required."));
82
+ console.log(formatInfo("Run 'gitforest login' to authenticate."));
83
+ process.exit(1);
84
+ }
85
+
86
+ console.log(formatInfo(`\nArchiving ${repos.length} repositories...\n`));
87
+
88
+ let success = 0;
89
+ for (const repo of repos) {
90
+ const result = await defaultGitHubService.archiveRepo(repo);
91
+ console.log(formatOperationItem(repo, result.success, result.error));
92
+ if (result.success) success++;
93
+ }
94
+
95
+ console.log(`\n${formatOperationSummary("Archived", success, repos.length)}`);
96
+ }
97
+
98
+ /**
99
+ * List GitHub repositories (not cloned locally)
100
+ */
101
+ export async function listGitHubRepos(options: CLIOptions & { view?: ViewMode }): Promise<void> {
102
+ const { config, filter, json, verbose, view = "github" } = options;
103
+
104
+ // Check for GitHub token - auto-login if not set
105
+ const token = await ensureAuthenticated();
106
+ if (!token) {
107
+ console.log(formatError("GitHub authentication required."));
108
+ console.log(formatInfo("Run 'gitforest login' to authenticate."));
109
+ process.exit(1);
110
+ }
111
+
112
+ console.log(formatScanning("Scanning local directories..."));
113
+ const localProjects = await scanAllDirectories(config);
114
+
115
+ console.log(formatScanning("Fetching GitHub repositories..."));
116
+ const { unified, error } = await fetchUnifiedRepos(localProjects);
117
+
118
+ if (error) {
119
+ console.log(formatWarning(error));
120
+ }
121
+
122
+ // Apply view filter
123
+ let result = filterByViewMode(unified, view);
124
+
125
+ // Apply text filter
126
+ if (filter) {
127
+ result = filterUnifiedRepos(result, filter);
128
+ }
129
+
130
+ // Sort
131
+ result = sortUnifiedRepos(result, config.display.sortBy, config.display.sortDirection);
132
+
133
+ const stats = getUnifiedStats(unified);
134
+ console.log(formatUnifiedRepoList(result, stats, view, { json, verbose }));
135
+ }
136
+
137
+ /**
138
+ * Show unified status (local + GitHub)
139
+ */
140
+ export async function showUnifiedStatus(options: CLIOptions): Promise<void> {
141
+ const { config, json } = options;
142
+
143
+ if (!defaultGitHubService.hasToken()) {
144
+ console.log(formatWarning("GITHUB_TOKEN not set - showing local status only"));
145
+ await showStatus(options);
146
+ return;
147
+ }
148
+
149
+ console.log(formatScanning("Scanning local directories..."));
150
+ const localProjects = await scanAllDirectories(config);
151
+
152
+ console.log(formatScanning("Fetching GitHub repositories..."));
153
+ const { unified, error } = await fetchUnifiedRepos(localProjects);
154
+
155
+ if (error) {
156
+ console.log(formatWarning(error));
157
+ }
158
+
159
+ const stats = getUnifiedStats(unified);
160
+ const statusData = {
161
+ stats,
162
+ githubOnly: unified.filter((r) => r.source === "github"),
163
+ localOnly: unified.filter((r) => r.source === "local"),
164
+ dirty: unified.filter((r) => r.local?.status?.isDirty),
165
+ unpushed: unified.filter((r) => r.local?.status?.isAhead),
166
+ unpulled: unified.filter((r) => r.local?.status?.isBehind),
167
+ };
168
+
169
+ if (json) {
170
+ console.log(formatUnifiedStatusJson(statusData));
171
+ return;
172
+ }
173
+
174
+ console.log(formatUnifiedStatusDisplay(statusData));
175
+ }
176
+
177
+ /**
178
+ * Clone GitHub repositories that aren't local
179
+ */
180
+ export async function cloneGitHubRepos(options: CLIOptions & {
181
+ repos?: string[];
182
+ targetDir?: string;
183
+ useHTTPS?: boolean;
184
+ }): Promise<void> {
185
+ const { config, filter, repos: specificRepos, targetDir, useHTTPS = false } = options;
186
+
187
+ // Check GitHub auth - auto-login if not set
188
+ const token = await ensureAuthenticated();
189
+ if (!token) {
190
+ console.log(formatError("GitHub authentication required."));
191
+ console.log(formatInfo("Run 'gitforest login' to authenticate."));
192
+ process.exit(1);
193
+ }
194
+
195
+ // Determine target directory
196
+ const cloneDir = targetDir ?? config.directories[0]?.path ?? process.cwd();
197
+
198
+ console.log(formatScanning("Scanning local directories..."));
199
+ const localProjects = await scanAllDirectories(config);
200
+
201
+ console.log(formatScanning("Fetching GitHub repositories..."));
202
+ const { unified, error } = await fetchUnifiedRepos(localProjects);
203
+
204
+ if (error) {
205
+ console.log(formatWarning(error));
206
+ }
207
+
208
+ // Get repos to clone
209
+ let toClone = unified.filter((r) => r.source === "github");
210
+
211
+ // Filter by specific repos if provided
212
+ if (specificRepos && specificRepos.length > 0) {
213
+ toClone = toClone.filter((r) =>
214
+ specificRepos.some((name) =>
215
+ r.name.toLowerCase() === name.toLowerCase() ||
216
+ r.github?.fullName.toLowerCase() === name.toLowerCase()
217
+ )
218
+ );
219
+ } else if (filter) {
220
+ toClone = filterUnifiedRepos(toClone, filter);
221
+ }
222
+
223
+ if (toClone.length === 0) {
224
+ console.log(formatWarning("No repositories to clone."));
225
+ return;
226
+ }
227
+
228
+ console.log(formatInfo(`\nCloning ${toClone.length} repositories to ${cloneDir}...\n`));
229
+
230
+ let success = 0;
231
+ for (const repo of toClone) {
232
+ const result = await cloneGitHubRepo(repo, cloneDir, !useHTTPS);
233
+ console.log(formatCloneItem(repo.github?.fullName, result.success, result.path, result.error));
234
+ if (result.success) success++;
235
+ }
236
+
237
+ console.log(`\n${formatOperationSummary("Cloned", success, toClone.length)}`);
238
+ }
@@ -0,0 +1,15 @@
1
+ import type { GitforestConfig, Project } from "../../types/index.ts";
2
+ import { scanAllDirectories, filterProjects } from "../../scanner/index.ts";
3
+ import { formatScanning } from "../formatters.ts";
4
+
5
+ /**
6
+ * Scan all directories and optionally filter results
7
+ */
8
+ export async function scanAndFilter(config: GitforestConfig, filter?: string): Promise<Project[]> {
9
+ console.log(formatScanning());
10
+ const projects = await scanAllDirectories(config);
11
+ if (filter) {
12
+ return filterProjects(projects, filter);
13
+ }
14
+ return projects;
15
+ }
@@ -0,0 +1,76 @@
1
+ import type { GitforestConfig } from "../../types/index.ts";
2
+ import { scanAllDirectories, sortProjects, filterProjects } from "../../scanner/index.ts";
3
+ import {
4
+ formatProjectList,
5
+ formatStatusSummary,
6
+ formatStatusSummaryJson,
7
+ formatDirtyRepos,
8
+ formatScanning,
9
+ calculateStatusSummary,
10
+ } from "../formatters.ts";
11
+
12
+ /**
13
+ * CLI command handlers for non-TUI usage
14
+ */
15
+
16
+ export interface CLIOptions {
17
+ config: GitforestConfig;
18
+ filter?: string;
19
+ json?: boolean;
20
+ verbose?: boolean;
21
+ maxDepth?: number;
22
+ }
23
+
24
+ /**
25
+ * List all projects
26
+ */
27
+ export async function listProjects(options: CLIOptions): Promise<void> {
28
+ const { config, filter, json, verbose } = options;
29
+
30
+ console.log(formatScanning());
31
+ const projects = await scanAllDirectories(config);
32
+ let result = sortProjects(projects, config.display.sortBy, config.display.sortDirection);
33
+
34
+ if (filter) {
35
+ result = filterProjects(result, filter);
36
+ }
37
+
38
+ console.log(formatProjectList(result, { json, verbose }));
39
+ }
40
+
41
+ /**
42
+ * Show status summary
43
+ */
44
+ export async function showStatus(options: CLIOptions): Promise<void> {
45
+ const { config, filter, json } = options;
46
+
47
+ console.log(formatScanning());
48
+ const projects = await scanAllDirectories(config);
49
+ let result = sortProjects(projects, "status", "desc");
50
+
51
+ if (filter) {
52
+ result = filterProjects(result, filter);
53
+ }
54
+
55
+ const summary = calculateStatusSummary(result);
56
+
57
+ if (json) {
58
+ console.log(formatStatusSummaryJson(summary));
59
+ return;
60
+ }
61
+
62
+ console.log(formatStatusSummary(summary));
63
+ }
64
+
65
+ /**
66
+ * Show dirty repositories
67
+ */
68
+ export async function showDirty(options: CLIOptions): Promise<void> {
69
+ const { config, json } = options;
70
+
71
+ console.log(formatScanning());
72
+ const projects = await scanAllDirectories(config);
73
+ const dirty = projects.filter((p) => p.status?.isDirty);
74
+
75
+ console.log(formatDirtyRepos(dirty, json));
76
+ }
@@ -0,0 +1,86 @@
1
+ import type { CLIOptions } from "./list.ts";
2
+ import { scanAllDirectories, filterProjects } from "../../scanner/index.ts";
3
+ import { initGitInProject } from "../../git/operations.ts";
4
+ import { defaultGitHubService } from "../../services/github.ts";
5
+ import { ensureAuthenticated } from "../../github/auth.ts";
6
+ import {
7
+ formatWarning,
8
+ formatError,
9
+ formatInfo,
10
+ formatScanning,
11
+ formatOperationItem,
12
+ formatOperationSummary,
13
+ } from "../formatters.ts";
14
+
15
+ /**
16
+ * Setup projects: init git + create GitHub repo + push
17
+ * For non-git projects and git projects without remotes
18
+ */
19
+ export async function setupProjects(options: CLIOptions & { isPrivate?: boolean }): Promise<void> {
20
+ const { config, filter, isPrivate = true } = options;
21
+
22
+ // Check GitHub auth - auto-login if not set
23
+ const token = await ensureAuthenticated();
24
+ if (!token) {
25
+ console.log(formatError("GitHub authentication required."));
26
+ console.log(formatInfo("Run 'gitforest login' to authenticate."));
27
+ process.exit(1);
28
+ }
29
+
30
+ console.log(formatScanning());
31
+ const projects = await scanAllDirectories(config);
32
+
33
+ // Find eligible projects: non-git OR git without remote
34
+ let eligibleProjects = projects.filter(
35
+ (p) => p.type === "non-git" || (p.type === "git" && !p.status?.hasRemote)
36
+ );
37
+
38
+ if (filter) {
39
+ eligibleProjects = filterProjects(eligibleProjects, filter);
40
+ }
41
+
42
+ if (eligibleProjects.length === 0) {
43
+ console.log(formatWarning("No projects need setup (all have remotes)."));
44
+ return;
45
+ }
46
+
47
+ const nonGitCount = eligibleProjects.filter((p) => p.type === "non-git").length;
48
+ const noRemoteCount = eligibleProjects.filter((p) => p.type === "git").length;
49
+
50
+ console.log(formatInfo(`\nSetting up ${eligibleProjects.length} projects:`));
51
+ if (nonGitCount > 0) console.log(formatInfo(` - ${nonGitCount} non-git projects (will init)`));
52
+ if (noRemoteCount > 0) console.log(formatInfo(` - ${noRemoteCount} git projects without remote`));
53
+ console.log(formatInfo(` - Creating ${isPrivate ? "private" : "public"} repos\n`));
54
+
55
+ let initSuccess = 0;
56
+ let createSuccess = 0;
57
+
58
+ for (const project of eligibleProjects) {
59
+ // Step 1: Init git if needed
60
+ if (project.type === "non-git") {
61
+ const initResult = await initGitInProject(project.path);
62
+ if (initResult.success) {
63
+ initSuccess++;
64
+ console.log(formatOperationItem(`${project.name} (init)`, true));
65
+ } else {
66
+ console.log(formatOperationItem(`${project.name} (init)`, false, initResult.error));
67
+ continue; // Skip create if init failed
68
+ }
69
+ }
70
+
71
+ // Step 2: Create GitHub repo (this also adds remote and pushes)
72
+ const createResult = await defaultGitHubService.createRepo({
73
+ name: project.name,
74
+ isPrivate,
75
+ localPath: project.path,
76
+ });
77
+
78
+ console.log(formatOperationItem(`${project.name} (create + push)`, createResult.success, createResult.error));
79
+ if (createResult.success) createSuccess++;
80
+ }
81
+
82
+ console.log(`\n${formatOperationSummary("Set up", createSuccess, eligibleProjects.length)}`);
83
+ if (nonGitCount > 0) {
84
+ console.log(formatInfo(` Git initialized: ${initSuccess}/${nonGitCount}`));
85
+ }
86
+ }