specweave 1.0.0-rc.2 → 1.0.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.
- package/dist/src/cli/workers/clone-worker.d.ts +7 -0
- package/dist/src/cli/workers/clone-worker.d.ts.map +1 -1
- package/dist/src/cli/workers/clone-worker.js +49 -18
- package/dist/src/cli/workers/clone-worker.js.map +1 -1
- package/dist/src/config/types.d.ts +1208 -203
- package/dist/src/config/types.d.ts.map +1 -1
- package/dist/src/init/architecture/types.d.ts +140 -33
- package/dist/src/init/architecture/types.d.ts.map +1 -1
- package/dist/src/init/compliance/types.d.ts +27 -30
- package/dist/src/init/compliance/types.d.ts.map +1 -1
- package/dist/src/init/repo/types.d.ts +34 -11
- package/dist/src/init/repo/types.d.ts.map +1 -1
- package/dist/src/init/research/src/config/types.d.ts +82 -15
- package/dist/src/init/research/src/config/types.d.ts.map +1 -1
- package/dist/src/init/research/types.d.ts +93 -38
- package/dist/src/init/research/types.d.ts.map +1 -1
- package/dist/src/init/team/types.d.ts +42 -4
- package/dist/src/init/team/types.d.ts.map +1 -1
- package/package.json +1 -1
- package/plugins/specweave/hooks/universal/hook-wrapper.cmd +26 -26
- package/plugins/specweave/hooks/universal/session-start.cmd +16 -16
- package/plugins/specweave/hooks/universal/session-start.ps1 +16 -16
- package/plugins/specweave-github/commands/clone.md +509 -0
- package/plugins/specweave-github/hooks/.specweave/logs/hooks-debug.log +738 -0
- package/plugins/specweave-release/hooks/.specweave/logs/dora-tracking.log +1107 -0
|
@@ -45,13 +45,51 @@ export declare const TeamRecommendationSchema: z.ZodObject<{
|
|
|
45
45
|
required: z.ZodBoolean;
|
|
46
46
|
reason: z.ZodString;
|
|
47
47
|
size: z.ZodString;
|
|
48
|
-
skills: z.ZodArray<z.ZodString>;
|
|
48
|
+
skills: z.ZodArray<z.ZodString, "many">;
|
|
49
49
|
serverlessAlternative: z.ZodOptional<z.ZodObject<{
|
|
50
50
|
service: z.ZodString;
|
|
51
51
|
costSavings: z.ZodNumber;
|
|
52
|
-
tradeoffs: z.ZodArray<z.ZodString>;
|
|
52
|
+
tradeoffs: z.ZodArray<z.ZodString, "many">;
|
|
53
53
|
pricingModel: z.ZodOptional<z.ZodString>;
|
|
54
|
-
}, z.
|
|
54
|
+
}, "strip", z.ZodTypeAny, {
|
|
55
|
+
service?: string;
|
|
56
|
+
costSavings?: number;
|
|
57
|
+
tradeoffs?: string[];
|
|
58
|
+
pricingModel?: string;
|
|
59
|
+
}, {
|
|
60
|
+
service?: string;
|
|
61
|
+
costSavings?: number;
|
|
62
|
+
tradeoffs?: string[];
|
|
63
|
+
pricingModel?: string;
|
|
64
|
+
}>>;
|
|
55
65
|
priority: z.ZodOptional<z.ZodNumber>;
|
|
56
|
-
}, z.
|
|
66
|
+
}, "strip", z.ZodTypeAny, {
|
|
67
|
+
skills?: string[];
|
|
68
|
+
reason?: string;
|
|
69
|
+
priority?: number;
|
|
70
|
+
size?: string;
|
|
71
|
+
role?: string;
|
|
72
|
+
teamName?: string;
|
|
73
|
+
required?: boolean;
|
|
74
|
+
serverlessAlternative?: {
|
|
75
|
+
service?: string;
|
|
76
|
+
costSavings?: number;
|
|
77
|
+
tradeoffs?: string[];
|
|
78
|
+
pricingModel?: string;
|
|
79
|
+
};
|
|
80
|
+
}, {
|
|
81
|
+
skills?: string[];
|
|
82
|
+
reason?: string;
|
|
83
|
+
priority?: number;
|
|
84
|
+
size?: string;
|
|
85
|
+
role?: string;
|
|
86
|
+
teamName?: string;
|
|
87
|
+
required?: boolean;
|
|
88
|
+
serverlessAlternative?: {
|
|
89
|
+
service?: string;
|
|
90
|
+
costSavings?: number;
|
|
91
|
+
tradeoffs?: string[];
|
|
92
|
+
pricingModel?: string;
|
|
93
|
+
};
|
|
94
|
+
}>;
|
|
57
95
|
//# sourceMappingURL=types.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../../src/init/team/types.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB;;GAEG;AACH,MAAM,WAAW,qBAAqB;IACpC,mDAAmD;IACnD,OAAO,EAAE,MAAM,CAAC;IAEhB,gDAAgD;IAChD,WAAW,EAAE,MAAM,CAAC;IAEpB,oCAAoC;IACpC,SAAS,EAAE,MAAM,EAAE,CAAC;IAEpB,oBAAoB;IACpB,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAED;;GAEG;AACH,MAAM,WAAW,kBAAkB;IACjC,2BAA2B;IAC3B,QAAQ,EAAE,MAAM,CAAC;IAEjB,0BAA0B;IAC1B,IAAI,EAAE,MAAM,CAAC;IAEb,+DAA+D;IAC/D,QAAQ,EAAE,OAAO,CAAC;IAElB,8BAA8B;IAC9B,MAAM,EAAE,MAAM,CAAC;IAEf,4BAA4B;IAC5B,IAAI,EAAE,MAAM,CAAC;IAEb,oCAAoC;IACpC,MAAM,EAAE,MAAM,EAAE,CAAC;IAEjB,6CAA6C;IAC7C,qBAAqB,CAAC,EAAE,qBAAqB,CAAC;IAE9C,yCAAyC;IACzC,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED;;GAEG;AACH,eAAO,MAAM,wBAAwB
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../../src/init/team/types.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB;;GAEG;AACH,MAAM,WAAW,qBAAqB;IACpC,mDAAmD;IACnD,OAAO,EAAE,MAAM,CAAC;IAEhB,gDAAgD;IAChD,WAAW,EAAE,MAAM,CAAC;IAEpB,oCAAoC;IACpC,SAAS,EAAE,MAAM,EAAE,CAAC;IAEpB,oBAAoB;IACpB,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAED;;GAEG;AACH,MAAM,WAAW,kBAAkB;IACjC,2BAA2B;IAC3B,QAAQ,EAAE,MAAM,CAAC;IAEjB,0BAA0B;IAC1B,IAAI,EAAE,MAAM,CAAC;IAEb,+DAA+D;IAC/D,QAAQ,EAAE,OAAO,CAAC;IAElB,8BAA8B;IAC9B,MAAM,EAAE,MAAM,CAAC;IAEf,4BAA4B;IAC5B,IAAI,EAAE,MAAM,CAAC;IAEb,oCAAoC;IACpC,MAAM,EAAE,MAAM,EAAE,CAAC;IAEjB,6CAA6C;IAC7C,qBAAqB,CAAC,EAAE,qBAAqB,CAAC;IAE9C,yCAAyC;IACzC,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED;;GAEG;AACH,eAAO,MAAM,wBAAwB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAcnC,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "specweave",
|
|
3
|
-
"version": "1.0.0
|
|
3
|
+
"version": "1.0.0",
|
|
4
4
|
"description": "Spec-driven development framework for Claude Code. AI-native workflow with living documentation, intelligent agents, and multilingual support (9 languages). Enterprise-grade traceability with permanent specs and temporary increments.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -1,26 +1,26 @@
|
|
|
1
|
-
@echo off
|
|
2
|
-
REM hook-wrapper.cmd - Windows resilient hook launcher
|
|
3
|
-
REM Prevents crashes when dispatcher.mjs is temporarily unavailable
|
|
4
|
-
|
|
5
|
-
setlocal enabledelayedexpansion
|
|
6
|
-
|
|
7
|
-
set "HOOK_TYPE=%~1"
|
|
8
|
-
if "%HOOK_TYPE%"=="" set "HOOK_TYPE=unknown"
|
|
9
|
-
|
|
10
|
-
set "SCRIPT_DIR=%~dp0"
|
|
11
|
-
set "DISPATCHER=%SCRIPT_DIR%dispatcher.mjs"
|
|
12
|
-
|
|
13
|
-
REM Check if dispatcher exists
|
|
14
|
-
if not exist "%DISPATCHER%" (
|
|
15
|
-
echo {"continue":true,"systemMessage":"Hook skipped: dispatcher.mjs not found"}
|
|
16
|
-
exit /b 0
|
|
17
|
-
)
|
|
18
|
-
|
|
19
|
-
REM Run dispatcher with error suppression
|
|
20
|
-
node "%DISPATCHER%" "%HOOK_TYPE%" 2>nul
|
|
21
|
-
if errorlevel 1 (
|
|
22
|
-
echo {"continue":true,"systemMessage":"Hook error, continuing"}
|
|
23
|
-
exit /b 0
|
|
24
|
-
)
|
|
25
|
-
|
|
26
|
-
exit /b 0
|
|
1
|
+
@echo off
|
|
2
|
+
REM hook-wrapper.cmd - Windows resilient hook launcher
|
|
3
|
+
REM Prevents crashes when dispatcher.mjs is temporarily unavailable
|
|
4
|
+
|
|
5
|
+
setlocal enabledelayedexpansion
|
|
6
|
+
|
|
7
|
+
set "HOOK_TYPE=%~1"
|
|
8
|
+
if "%HOOK_TYPE%"=="" set "HOOK_TYPE=unknown"
|
|
9
|
+
|
|
10
|
+
set "SCRIPT_DIR=%~dp0"
|
|
11
|
+
set "DISPATCHER=%SCRIPT_DIR%dispatcher.mjs"
|
|
12
|
+
|
|
13
|
+
REM Check if dispatcher exists
|
|
14
|
+
if not exist "%DISPATCHER%" (
|
|
15
|
+
echo {"continue":true,"systemMessage":"Hook skipped: dispatcher.mjs not found"}
|
|
16
|
+
exit /b 0
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
REM Run dispatcher with error suppression
|
|
20
|
+
node "%DISPATCHER%" "%HOOK_TYPE%" 2>nul
|
|
21
|
+
if errorlevel 1 (
|
|
22
|
+
echo {"continue":true,"systemMessage":"Hook error, continuing"}
|
|
23
|
+
exit /b 0
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
exit /b 0
|
|
@@ -1,16 +1,16 @@
|
|
|
1
|
-
@echo off
|
|
2
|
-
:: Universal Session Start Hook for Windows
|
|
3
|
-
:: Calls the Node.js dispatcher
|
|
4
|
-
|
|
5
|
-
:: Find node.exe
|
|
6
|
-
where node >nul 2>&1
|
|
7
|
-
if %ERRORLEVEL% neq 0 (
|
|
8
|
-
echo {"continue": true, "error": "Node.js not found"}
|
|
9
|
-
exit /b 0
|
|
10
|
-
)
|
|
11
|
-
|
|
12
|
-
:: Get the directory of this script
|
|
13
|
-
set "SCRIPT_DIR=%~dp0"
|
|
14
|
-
|
|
15
|
-
:: Run the dispatcher
|
|
16
|
-
node "%SCRIPT_DIR%dispatcher.mjs" session-start
|
|
1
|
+
@echo off
|
|
2
|
+
:: Universal Session Start Hook for Windows
|
|
3
|
+
:: Calls the Node.js dispatcher
|
|
4
|
+
|
|
5
|
+
:: Find node.exe
|
|
6
|
+
where node >nul 2>&1
|
|
7
|
+
if %ERRORLEVEL% neq 0 (
|
|
8
|
+
echo {"continue": true, "error": "Node.js not found"}
|
|
9
|
+
exit /b 0
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
:: Get the directory of this script
|
|
13
|
+
set "SCRIPT_DIR=%~dp0"
|
|
14
|
+
|
|
15
|
+
:: Run the dispatcher
|
|
16
|
+
node "%SCRIPT_DIR%dispatcher.mjs" session-start
|
|
@@ -1,16 +1,16 @@
|
|
|
1
|
-
# Universal Session Start Hook for Windows PowerShell
|
|
2
|
-
# Calls the Node.js dispatcher for cross-platform compatibility
|
|
3
|
-
|
|
4
|
-
# Find node.exe
|
|
5
|
-
$nodePath = Get-Command node -ErrorAction SilentlyContinue
|
|
6
|
-
|
|
7
|
-
if (-not $nodePath) {
|
|
8
|
-
Write-Host '{"continue": true, "error": "Node.js not found"}'
|
|
9
|
-
exit 0
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
# Get script directory
|
|
13
|
-
$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
|
|
14
|
-
|
|
15
|
-
# Run the dispatcher
|
|
16
|
-
& node "$scriptDir\dispatcher.mjs" session-start
|
|
1
|
+
# Universal Session Start Hook for Windows PowerShell
|
|
2
|
+
# Calls the Node.js dispatcher for cross-platform compatibility
|
|
3
|
+
|
|
4
|
+
# Find node.exe
|
|
5
|
+
$nodePath = Get-Command node -ErrorAction SilentlyContinue
|
|
6
|
+
|
|
7
|
+
if (-not $nodePath) {
|
|
8
|
+
Write-Host '{"continue": true, "error": "Node.js not found"}'
|
|
9
|
+
exit 0
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
# Get script directory
|
|
13
|
+
$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
|
|
14
|
+
|
|
15
|
+
# Run the dispatcher
|
|
16
|
+
& node "$scriptDir\dispatcher.mjs" session-start
|
|
@@ -0,0 +1,509 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: sw-github:clone
|
|
3
|
+
description: Clone GitHub repositories to local workspace. Use after init if cloning was skipped, to resume interrupted cloning, or to add repos later. Already-cloned repos are automatically skipped.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Clone GitHub Repositories Command
|
|
7
|
+
|
|
8
|
+
You are a GitHub repository cloning expert. Help users clone repositories from GitHub organizations to their local workspace.
|
|
9
|
+
|
|
10
|
+
## Purpose
|
|
11
|
+
|
|
12
|
+
This command clones GitHub repositories **after** initial SpecWeave setup (`specweave init`). Use when:
|
|
13
|
+
- User skipped cloning during init
|
|
14
|
+
- **Resuming interrupted cloning** (already-cloned repos are skipped!)
|
|
15
|
+
- Adding repositories from organization
|
|
16
|
+
- Selective cloning with pattern filtering
|
|
17
|
+
- Retrying after partial failures
|
|
18
|
+
|
|
19
|
+
## CRITICAL: NEVER-STOP BEHAVIOR
|
|
20
|
+
|
|
21
|
+
**This command NEVER stops on individual repo failures!**
|
|
22
|
+
- Each repo failure is logged but cloning continues
|
|
23
|
+
- Already-cloned repos are automatically skipped (resume = re-run!)
|
|
24
|
+
- Final status: `completed` (all success) or `completed_with_warnings` (some failed)
|
|
25
|
+
- Failed repos are listed in result.json for easy retry
|
|
26
|
+
|
|
27
|
+
## Command Syntax
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
# Interactive mode (prompts for everything)
|
|
31
|
+
/sw-github:clone
|
|
32
|
+
|
|
33
|
+
# Clone from specific org
|
|
34
|
+
/sw-github:clone --org "mycompany"
|
|
35
|
+
|
|
36
|
+
# With pattern filter (glob)
|
|
37
|
+
/sw-github:clone --pattern "api-*"
|
|
38
|
+
|
|
39
|
+
# Regex pattern
|
|
40
|
+
/sw-github:clone --pattern "regex:^frontend-.*$"
|
|
41
|
+
|
|
42
|
+
# Dry-run (preview only)
|
|
43
|
+
/sw-github:clone --dry-run
|
|
44
|
+
|
|
45
|
+
# Resume/retry - just run again! Already cloned repos are skipped
|
|
46
|
+
/sw-github:clone
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## Your Task
|
|
50
|
+
|
|
51
|
+
When the user runs this command:
|
|
52
|
+
|
|
53
|
+
### Step 1: Check Prerequisites
|
|
54
|
+
|
|
55
|
+
```typescript
|
|
56
|
+
import { readEnvFile, parseEnvFile } from '../../../src/utils/env-file.js';
|
|
57
|
+
import chalk from 'chalk';
|
|
58
|
+
|
|
59
|
+
const projectPath = process.cwd();
|
|
60
|
+
|
|
61
|
+
// Check for GitHub token
|
|
62
|
+
const token = process.env.GH_TOKEN || process.env.GITHUB_TOKEN;
|
|
63
|
+
|
|
64
|
+
if (!token) {
|
|
65
|
+
// Try .env file
|
|
66
|
+
const envContent = readEnvFile(projectPath);
|
|
67
|
+
if (envContent) {
|
|
68
|
+
const parsed = parseEnvFile(envContent);
|
|
69
|
+
if (parsed.GH_TOKEN || parsed.GITHUB_TOKEN) {
|
|
70
|
+
// Token found in .env
|
|
71
|
+
} else {
|
|
72
|
+
console.log(chalk.red('❌ No GitHub token found.'));
|
|
73
|
+
console.log(chalk.gray(' Set GH_TOKEN or GITHUB_TOKEN environment variable.'));
|
|
74
|
+
console.log(chalk.gray(' Or add to .env file: GH_TOKEN=ghp_xxxx'));
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
} else {
|
|
78
|
+
console.log(chalk.red('❌ No GitHub token found.'));
|
|
79
|
+
console.log(chalk.gray(' Set GH_TOKEN or GITHUB_TOKEN environment variable.'));
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
console.log(chalk.blue('\n📦 GitHub Repository Cloning\n'));
|
|
85
|
+
console.log(chalk.green(' ✓ GitHub token found'));
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
### Step 2: Get Organization
|
|
89
|
+
|
|
90
|
+
```typescript
|
|
91
|
+
import { input } from '@inquirer/prompts';
|
|
92
|
+
|
|
93
|
+
let org = args.org;
|
|
94
|
+
|
|
95
|
+
if (!org) {
|
|
96
|
+
// Try to detect from config
|
|
97
|
+
const configPath = path.join(projectPath, '.specweave', 'config.json');
|
|
98
|
+
if (fs.existsSync(configPath)) {
|
|
99
|
+
const config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
|
|
100
|
+
if (config.github?.org) {
|
|
101
|
+
org = config.github.org;
|
|
102
|
+
console.log(chalk.gray(` Organization: ${org} (from config)`));
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (!org) {
|
|
107
|
+
// Try to detect from git remote
|
|
108
|
+
const { execSync } = await import('child_process');
|
|
109
|
+
try {
|
|
110
|
+
const remote = execSync('git remote get-url origin', { encoding: 'utf-8' }).trim();
|
|
111
|
+
const match = remote.match(/github\.com[:/]([^/]+)/);
|
|
112
|
+
if (match) {
|
|
113
|
+
org = match[1];
|
|
114
|
+
console.log(chalk.gray(` Detected org from git remote: ${org}`));
|
|
115
|
+
}
|
|
116
|
+
} catch {}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (!org) {
|
|
120
|
+
org = await input({
|
|
121
|
+
message: 'Enter GitHub organization or username:',
|
|
122
|
+
validate: v => v.trim() ? true : 'Organization required'
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
console.log(chalk.gray(` Organization: ${org}`));
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
### Step 3: Fetch Repositories
|
|
131
|
+
|
|
132
|
+
```typescript
|
|
133
|
+
console.log(chalk.gray('\n Fetching repositories...'));
|
|
134
|
+
|
|
135
|
+
// Fetch repos from GitHub API with pagination
|
|
136
|
+
const pat = process.env.GH_TOKEN || process.env.GITHUB_TOKEN;
|
|
137
|
+
const allRepos = [];
|
|
138
|
+
let page = 1;
|
|
139
|
+
const perPage = 100;
|
|
140
|
+
|
|
141
|
+
while (true) {
|
|
142
|
+
// Try org repos first, fall back to user repos
|
|
143
|
+
let response = await fetch(
|
|
144
|
+
`https://api.github.com/orgs/${encodeURIComponent(org)}/repos?per_page=${perPage}&page=${page}`,
|
|
145
|
+
{
|
|
146
|
+
headers: {
|
|
147
|
+
'Authorization': `Bearer ${pat}`,
|
|
148
|
+
'Accept': 'application/vnd.github+json',
|
|
149
|
+
'X-GitHub-Api-Version': '2022-11-28'
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
);
|
|
153
|
+
|
|
154
|
+
if (response.status === 404) {
|
|
155
|
+
// Try as user
|
|
156
|
+
response = await fetch(
|
|
157
|
+
`https://api.github.com/users/${encodeURIComponent(org)}/repos?per_page=${perPage}&page=${page}`,
|
|
158
|
+
{
|
|
159
|
+
headers: {
|
|
160
|
+
'Authorization': `Bearer ${pat}`,
|
|
161
|
+
'Accept': 'application/vnd.github+json',
|
|
162
|
+
'X-GitHub-Api-Version': '2022-11-28'
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
if (!response.ok) {
|
|
169
|
+
console.log(chalk.red(`❌ Failed to fetch repos: ${response.status}`));
|
|
170
|
+
const error = await response.text();
|
|
171
|
+
console.log(chalk.gray(` ${error}`));
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const batch = await response.json();
|
|
176
|
+
if (batch.length === 0) break;
|
|
177
|
+
|
|
178
|
+
allRepos.push(...batch);
|
|
179
|
+
|
|
180
|
+
if (batch.length < perPage) break;
|
|
181
|
+
page++;
|
|
182
|
+
|
|
183
|
+
// Progress for large orgs
|
|
184
|
+
if (allRepos.length % 100 === 0) {
|
|
185
|
+
console.log(chalk.gray(` Fetched ${allRepos.length} repos...`));
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
if (allRepos.length === 0) {
|
|
190
|
+
console.log(chalk.yellow('\n⚠️ No repositories found.'));
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
console.log(chalk.green(` ✓ Found ${allRepos.length} repositories in ${org}`));
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
### Step 4: Check Already Cloned (Resume Detection)
|
|
198
|
+
|
|
199
|
+
```typescript
|
|
200
|
+
import * as fs from 'fs';
|
|
201
|
+
import * as path from 'path';
|
|
202
|
+
|
|
203
|
+
// Check which repos already exist
|
|
204
|
+
const alreadyCloned = [];
|
|
205
|
+
const needsCloning = [];
|
|
206
|
+
|
|
207
|
+
for (const repo of allRepos) {
|
|
208
|
+
const repoPath = path.join(projectPath, repo.name, '.git');
|
|
209
|
+
if (fs.existsSync(repoPath)) {
|
|
210
|
+
alreadyCloned.push(repo);
|
|
211
|
+
} else {
|
|
212
|
+
needsCloning.push(repo);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
if (alreadyCloned.length > 0) {
|
|
217
|
+
console.log(chalk.cyan(`\n 📂 Already cloned: ${alreadyCloned.length} repos (will be skipped)`));
|
|
218
|
+
if (alreadyCloned.length <= 5) {
|
|
219
|
+
alreadyCloned.forEach(r => console.log(chalk.gray(` ✓ ${r.name}`)));
|
|
220
|
+
} else {
|
|
221
|
+
alreadyCloned.slice(0, 3).forEach(r => console.log(chalk.gray(` ✓ ${r.name}`)));
|
|
222
|
+
console.log(chalk.gray(` ... and ${alreadyCloned.length - 3} more`));
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
console.log(chalk.blue(`\n 📦 Need to clone: ${needsCloning.length} repos`));
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
### Step 5: Apply Pattern Filter
|
|
230
|
+
|
|
231
|
+
```typescript
|
|
232
|
+
import { filterRepositoriesByPattern } from '../../../src/cli/helpers/selection-strategy.js';
|
|
233
|
+
import { select, input } from '@inquirer/prompts';
|
|
234
|
+
|
|
235
|
+
let filteredRepos = needsCloning;
|
|
236
|
+
let patternDescription = 'all';
|
|
237
|
+
|
|
238
|
+
if (args.pattern) {
|
|
239
|
+
const isRegex = args.pattern.startsWith('regex:');
|
|
240
|
+
const pattern = isRegex ? args.pattern.slice(6) : args.pattern;
|
|
241
|
+
|
|
242
|
+
const clonePattern = {
|
|
243
|
+
strategy: isRegex ? 'pattern-regex' : 'pattern-glob',
|
|
244
|
+
pattern: pattern,
|
|
245
|
+
isRegex
|
|
246
|
+
};
|
|
247
|
+
|
|
248
|
+
filteredRepos = filterRepositoriesByPattern(needsCloning, clonePattern);
|
|
249
|
+
patternDescription = `matching "${pattern}"`;
|
|
250
|
+
|
|
251
|
+
console.log(chalk.gray(` Pattern: ${args.pattern}`));
|
|
252
|
+
console.log(chalk.gray(` Matched: ${filteredRepos.length} of ${needsCloning.length} repos\n`));
|
|
253
|
+
} else if (needsCloning.length > 0) {
|
|
254
|
+
const strategy = await select({
|
|
255
|
+
message: 'How do you want to select repositories?',
|
|
256
|
+
choices: [
|
|
257
|
+
{ name: 'All - Clone all repositories', value: 'all' },
|
|
258
|
+
{ name: 'Pattern (glob) - e.g., "api-*", "*-backend"', value: 'pattern-glob' },
|
|
259
|
+
{ name: 'Pattern (regex) - e.g., "^api-.*$"', value: 'pattern-regex' }
|
|
260
|
+
]
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
if (strategy !== 'all') {
|
|
264
|
+
const pattern = await input({
|
|
265
|
+
message: 'Enter pattern:',
|
|
266
|
+
validate: v => v.trim() ? true : 'Pattern required'
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
const clonePattern = {
|
|
270
|
+
strategy,
|
|
271
|
+
pattern: pattern.trim(),
|
|
272
|
+
isRegex: strategy === 'pattern-regex'
|
|
273
|
+
};
|
|
274
|
+
|
|
275
|
+
filteredRepos = filterRepositoriesByPattern(needsCloning, clonePattern);
|
|
276
|
+
patternDescription = `matching "${pattern.trim()}"`;
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
if (filteredRepos.length === 0) {
|
|
281
|
+
if (alreadyCloned.length > 0) {
|
|
282
|
+
console.log(chalk.green(`\n✅ All ${alreadyCloned.length} repos already cloned. Nothing to do!`));
|
|
283
|
+
} else {
|
|
284
|
+
console.log(chalk.yellow(`\n⚠️ No repositories ${patternDescription}.`));
|
|
285
|
+
}
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
### Step 6: Preview and Confirm
|
|
291
|
+
|
|
292
|
+
```typescript
|
|
293
|
+
import { confirm } from '@inquirer/prompts';
|
|
294
|
+
|
|
295
|
+
console.log(chalk.blue(`\n📦 Repositories to clone (${filteredRepos.length}):\n`));
|
|
296
|
+
|
|
297
|
+
// Show preview (max 20)
|
|
298
|
+
filteredRepos.slice(0, 20).forEach(repo => {
|
|
299
|
+
console.log(chalk.gray(` • ${repo.name}`));
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
if (filteredRepos.length > 20) {
|
|
303
|
+
console.log(chalk.gray(` ... and ${filteredRepos.length - 20} more\n`));
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
if (args.dryRun) {
|
|
307
|
+
console.log(chalk.cyan('\n🔎 DRY RUN: No repositories will be cloned.\n'));
|
|
308
|
+
console.log(chalk.gray(' Remove --dry-run to actually clone.'));
|
|
309
|
+
return;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
const confirmed = await confirm({
|
|
313
|
+
message: `Clone ${filteredRepos.length} repositories to current directory?`,
|
|
314
|
+
default: true
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
if (!confirmed) {
|
|
318
|
+
console.log(chalk.gray('\n⏭️ Cloning cancelled.\n'));
|
|
319
|
+
return;
|
|
320
|
+
}
|
|
321
|
+
```
|
|
322
|
+
|
|
323
|
+
### Step 7: Start Background Cloning
|
|
324
|
+
|
|
325
|
+
```typescript
|
|
326
|
+
import { triggerGitHubRepoCloning } from '../../../src/cli/helpers/init/github-repo-cloning.js';
|
|
327
|
+
|
|
328
|
+
// Build selection
|
|
329
|
+
const githubRepoSelection = {
|
|
330
|
+
org,
|
|
331
|
+
pat: process.env.GH_TOKEN || process.env.GITHUB_TOKEN
|
|
332
|
+
};
|
|
333
|
+
|
|
334
|
+
// Build clonePattern
|
|
335
|
+
const clonePatternResult = args.pattern
|
|
336
|
+
? {
|
|
337
|
+
strategy: args.pattern.startsWith('regex:') ? 'pattern-regex' : 'pattern-glob',
|
|
338
|
+
pattern: args.pattern.startsWith('regex:') ? args.pattern.slice(6) : args.pattern
|
|
339
|
+
}
|
|
340
|
+
: { strategy: 'all' };
|
|
341
|
+
|
|
342
|
+
// Trigger background cloning
|
|
343
|
+
const jobId = await triggerGitHubRepoCloning(projectPath, githubRepoSelection, clonePatternResult);
|
|
344
|
+
|
|
345
|
+
if (jobId) {
|
|
346
|
+
console.log(chalk.green('\n✅ Clone job started successfully!\n'));
|
|
347
|
+
console.log(chalk.blue('📋 Key Points:'));
|
|
348
|
+
console.log(chalk.gray(' • Cloning runs in background - you can continue working'));
|
|
349
|
+
console.log(chalk.gray(' • Already-cloned repos are automatically skipped'));
|
|
350
|
+
console.log(chalk.gray(' • Individual failures do NOT stop the job'));
|
|
351
|
+
console.log(chalk.gray(' • To resume after interruption: just run /sw-github:clone again!\n'));
|
|
352
|
+
|
|
353
|
+
console.log(chalk.blue('🔧 Commands:'));
|
|
354
|
+
console.log(chalk.cyan(` /sw:jobs → Check progress`));
|
|
355
|
+
console.log(chalk.cyan(` /sw:jobs --follow ${jobId.slice(0, 8)} → Follow live`));
|
|
356
|
+
console.log(chalk.cyan(` /sw:jobs --logs ${jobId.slice(0, 8)} → View logs`));
|
|
357
|
+
}
|
|
358
|
+
```
|
|
359
|
+
|
|
360
|
+
## Examples
|
|
361
|
+
|
|
362
|
+
### Example 1: Fresh Clone
|
|
363
|
+
**User**: `/sw-github:clone --org olympusnova`
|
|
364
|
+
|
|
365
|
+
**Output**:
|
|
366
|
+
```
|
|
367
|
+
📦 GitHub Repository Cloning
|
|
368
|
+
|
|
369
|
+
✓ GitHub token found
|
|
370
|
+
Organization: olympusnova
|
|
371
|
+
|
|
372
|
+
Fetching repositories...
|
|
373
|
+
✓ Found 512 repositories in olympusnova
|
|
374
|
+
|
|
375
|
+
📦 Need to clone: 512 repos
|
|
376
|
+
|
|
377
|
+
How do you want to select repositories?
|
|
378
|
+
> All - Clone all repositories
|
|
379
|
+
|
|
380
|
+
📦 Repositories to clone (512):
|
|
381
|
+
|
|
382
|
+
• api-gateway
|
|
383
|
+
• frontend-web
|
|
384
|
+
• mobile-app
|
|
385
|
+
... and 509 more
|
|
386
|
+
|
|
387
|
+
Clone 512 repositories to current directory? (Y/n)
|
|
388
|
+
|
|
389
|
+
🔄 Starting background clone for 512 repositories...
|
|
390
|
+
|
|
391
|
+
✓ Clone job started in background (PID: 12345)
|
|
392
|
+
|
|
393
|
+
✅ Clone job started successfully!
|
|
394
|
+
|
|
395
|
+
📋 Key Points:
|
|
396
|
+
• Cloning runs in background - you can continue working
|
|
397
|
+
• Already-cloned repos are automatically skipped
|
|
398
|
+
• Individual failures do NOT stop the job
|
|
399
|
+
• To resume after interruption: just run /sw-github:clone again!
|
|
400
|
+
|
|
401
|
+
🔧 Commands:
|
|
402
|
+
/sw:jobs → Check progress
|
|
403
|
+
/sw:jobs --follow abc12345 → Follow live
|
|
404
|
+
/sw:jobs --logs abc12345 → View logs
|
|
405
|
+
```
|
|
406
|
+
|
|
407
|
+
### Example 2: Resume After Interruption
|
|
408
|
+
**User**: `/sw-github:clone` (after previous job was interrupted at 87/512)
|
|
409
|
+
|
|
410
|
+
**Output**:
|
|
411
|
+
```
|
|
412
|
+
📦 GitHub Repository Cloning
|
|
413
|
+
|
|
414
|
+
✓ GitHub token found
|
|
415
|
+
Organization: olympusnova (from config)
|
|
416
|
+
|
|
417
|
+
Fetching repositories...
|
|
418
|
+
✓ Found 512 repositories in olympusnova
|
|
419
|
+
|
|
420
|
+
📂 Already cloned: 87 repos (will be skipped)
|
|
421
|
+
✓ api-gateway
|
|
422
|
+
✓ frontend-web
|
|
423
|
+
✓ mobile-app
|
|
424
|
+
... and 84 more
|
|
425
|
+
|
|
426
|
+
📦 Need to clone: 425 repos
|
|
427
|
+
|
|
428
|
+
Clone 425 repositories to current directory? (Y/n)
|
|
429
|
+
|
|
430
|
+
🔄 Resuming clone for 425 remaining repositories...
|
|
431
|
+
```
|
|
432
|
+
|
|
433
|
+
### Example 3: Pattern Filter
|
|
434
|
+
**User**: `/sw-github:clone --pattern "api-*"`
|
|
435
|
+
|
|
436
|
+
**Output**:
|
|
437
|
+
```
|
|
438
|
+
📦 GitHub Repository Cloning
|
|
439
|
+
|
|
440
|
+
Pattern: api-*
|
|
441
|
+
Matched: 45 of 512 repos
|
|
442
|
+
|
|
443
|
+
📦 Repositories to clone (45):
|
|
444
|
+
|
|
445
|
+
• api-gateway
|
|
446
|
+
• api-auth
|
|
447
|
+
• api-payments
|
|
448
|
+
...
|
|
449
|
+
```
|
|
450
|
+
|
|
451
|
+
### Example 4: Dry Run
|
|
452
|
+
**User**: `/sw-github:clone --dry-run`
|
|
453
|
+
|
|
454
|
+
**Output**:
|
|
455
|
+
```
|
|
456
|
+
📦 Repositories to clone (512):
|
|
457
|
+
|
|
458
|
+
• api-gateway
|
|
459
|
+
• frontend-web
|
|
460
|
+
...
|
|
461
|
+
|
|
462
|
+
🔎 DRY RUN: No repositories will be cloned.
|
|
463
|
+
Remove --dry-run to actually clone.
|
|
464
|
+
```
|
|
465
|
+
|
|
466
|
+
## Important Notes
|
|
467
|
+
|
|
468
|
+
### Resume = Just Re-run!
|
|
469
|
+
**There's no special resume command.** Just run `/sw-github:clone` again:
|
|
470
|
+
- Already-cloned repos are detected via `.git` folder
|
|
471
|
+
- They're automatically skipped (instant, no API calls)
|
|
472
|
+
- Only remaining repos are queued for cloning
|
|
473
|
+
|
|
474
|
+
### Never Stops on Failure
|
|
475
|
+
- Each repo cloned independently
|
|
476
|
+
- Failures logged but don't stop the job
|
|
477
|
+
- Final status: `completed_with_warnings` if any failed
|
|
478
|
+
- Failed repo list saved to `result.json`
|
|
479
|
+
|
|
480
|
+
### Retry Failed Repos
|
|
481
|
+
After job completes with warnings:
|
|
482
|
+
```bash
|
|
483
|
+
# Check which repos failed
|
|
484
|
+
cat .specweave/state/jobs/<jobId>/result.json
|
|
485
|
+
|
|
486
|
+
# Just run clone again - it skips successful ones!
|
|
487
|
+
/sw-github:clone
|
|
488
|
+
```
|
|
489
|
+
|
|
490
|
+
## Related Commands
|
|
491
|
+
|
|
492
|
+
- `/sw:init` - Initial SpecWeave setup (includes repo cloning option)
|
|
493
|
+
- `/sw:jobs` - Monitor background jobs
|
|
494
|
+
- `/sw:jobs --follow <id>` - Watch progress live
|
|
495
|
+
- `/sw-github:sync` - Sync with GitHub Issues
|
|
496
|
+
|
|
497
|
+
## Error Handling
|
|
498
|
+
|
|
499
|
+
| Error | Cause | Solution |
|
|
500
|
+
|-------|-------|----------|
|
|
501
|
+
| No GitHub token | Missing env var | Set `GH_TOKEN` or `GITHUB_TOKEN` |
|
|
502
|
+
| 401 Unauthorized | Invalid token | Check token has `repo` scope |
|
|
503
|
+
| 404 Not Found | Wrong org name | Verify organization/username |
|
|
504
|
+
| Rate limit | Too many API calls | Wait and retry, or use pagination |
|
|
505
|
+
| Clone failure | Permission/network | Job continues, repo listed in failures |
|
|
506
|
+
|
|
507
|
+
---
|
|
508
|
+
|
|
509
|
+
**Multi-Repo Support**: Perfect for microservices architectures with many GitHub repositories.
|