argustack 0.1.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/LICENSE +21 -0
- package/README.md +433 -0
- package/dist/adapters/csv/index.d.ts +4 -0
- package/dist/adapters/csv/index.d.ts.map +1 -0
- package/dist/adapters/csv/index.js +4 -0
- package/dist/adapters/csv/index.js.map +1 -0
- package/dist/adapters/csv/mapper.d.ts +10 -0
- package/dist/adapters/csv/mapper.d.ts.map +1 -0
- package/dist/adapters/csv/mapper.js +179 -0
- package/dist/adapters/csv/mapper.js.map +1 -0
- package/dist/adapters/csv/parser.d.ts +22 -0
- package/dist/adapters/csv/parser.d.ts.map +1 -0
- package/dist/adapters/csv/parser.js +96 -0
- package/dist/adapters/csv/parser.js.map +1 -0
- package/dist/adapters/csv/provider.d.ts +11 -0
- package/dist/adapters/csv/provider.d.ts.map +1 -0
- package/dist/adapters/csv/provider.js +98 -0
- package/dist/adapters/csv/provider.js.map +1 -0
- package/dist/adapters/git/index.d.ts +3 -0
- package/dist/adapters/git/index.d.ts.map +1 -0
- package/dist/adapters/git/index.js +3 -0
- package/dist/adapters/git/index.js.map +1 -0
- package/dist/adapters/git/mapper.d.ts +11 -0
- package/dist/adapters/git/mapper.d.ts.map +1 -0
- package/dist/adapters/git/mapper.js +47 -0
- package/dist/adapters/git/mapper.js.map +1 -0
- package/dist/adapters/git/provider.d.ts +12 -0
- package/dist/adapters/git/provider.d.ts.map +1 -0
- package/dist/adapters/git/provider.js +115 -0
- package/dist/adapters/git/provider.js.map +1 -0
- package/dist/adapters/github/client.d.ts +8 -0
- package/dist/adapters/github/client.d.ts.map +1 -0
- package/dist/adapters/github/client.js +5 -0
- package/dist/adapters/github/client.js.map +1 -0
- package/dist/adapters/github/index.d.ts +4 -0
- package/dist/adapters/github/index.d.ts.map +1 -0
- package/dist/adapters/github/index.js +4 -0
- package/dist/adapters/github/index.js.map +1 -0
- package/dist/adapters/github/mapper.d.ts +12 -0
- package/dist/adapters/github/mapper.d.ts.map +1 -0
- package/dist/adapters/github/mapper.js +117 -0
- package/dist/adapters/github/mapper.js.map +1 -0
- package/dist/adapters/github/provider.d.ts +18 -0
- package/dist/adapters/github/provider.d.ts.map +1 -0
- package/dist/adapters/github/provider.js +95 -0
- package/dist/adapters/github/provider.js.map +1 -0
- package/dist/adapters/jira/client.d.ts +11 -0
- package/dist/adapters/jira/client.d.ts.map +1 -0
- package/dist/adapters/jira/client.js +16 -0
- package/dist/adapters/jira/client.js.map +1 -0
- package/dist/adapters/jira/index.d.ts +3 -0
- package/dist/adapters/jira/index.d.ts.map +1 -0
- package/dist/adapters/jira/index.js +3 -0
- package/dist/adapters/jira/index.js.map +1 -0
- package/dist/adapters/jira/mapper.d.ts +14 -0
- package/dist/adapters/jira/mapper.d.ts.map +1 -0
- package/dist/adapters/jira/mapper.js +169 -0
- package/dist/adapters/jira/mapper.js.map +1 -0
- package/dist/adapters/jira/provider.d.ts +21 -0
- package/dist/adapters/jira/provider.d.ts.map +1 -0
- package/dist/adapters/jira/provider.js +79 -0
- package/dist/adapters/jira/provider.js.map +1 -0
- package/dist/adapters/openai/embedding-provider.d.ts +16 -0
- package/dist/adapters/openai/embedding-provider.d.ts.map +1 -0
- package/dist/adapters/openai/embedding-provider.js +34 -0
- package/dist/adapters/openai/embedding-provider.js.map +1 -0
- package/dist/adapters/openai/index.d.ts +2 -0
- package/dist/adapters/openai/index.d.ts.map +1 -0
- package/dist/adapters/openai/index.js +2 -0
- package/dist/adapters/openai/index.js.map +1 -0
- package/dist/adapters/postgres/connection.d.ts +13 -0
- package/dist/adapters/postgres/connection.d.ts.map +1 -0
- package/dist/adapters/postgres/connection.js +16 -0
- package/dist/adapters/postgres/connection.js.map +1 -0
- package/dist/adapters/postgres/index.d.ts +3 -0
- package/dist/adapters/postgres/index.d.ts.map +1 -0
- package/dist/adapters/postgres/index.js +3 -0
- package/dist/adapters/postgres/index.js.map +1 -0
- package/dist/adapters/postgres/schema.d.ts +6 -0
- package/dist/adapters/postgres/schema.d.ts.map +1 -0
- package/dist/adapters/postgres/schema.js +246 -0
- package/dist/adapters/postgres/schema.js.map +1 -0
- package/dist/adapters/postgres/storage.d.ts +32 -0
- package/dist/adapters/postgres/storage.d.ts.map +1 -0
- package/dist/adapters/postgres/storage.js +322 -0
- package/dist/adapters/postgres/storage.js.map +1 -0
- package/dist/cli/embed.d.ts +3 -0
- package/dist/cli/embed.d.ts.map +1 -0
- package/dist/cli/embed.js +52 -0
- package/dist/cli/embed.js.map +1 -0
- package/dist/cli/index.d.ts +3 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +64 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/cli/init.d.ts +24 -0
- package/dist/cli/init.d.ts.map +1 -0
- package/dist/cli/init.js +966 -0
- package/dist/cli/init.js.map +1 -0
- package/dist/cli/jira.d.ts +3 -0
- package/dist/cli/jira.d.ts.map +1 -0
- package/dist/cli/jira.js +78 -0
- package/dist/cli/jira.js.map +1 -0
- package/dist/cli/mcp-install.d.ts +10 -0
- package/dist/cli/mcp-install.d.ts.map +1 -0
- package/dist/cli/mcp-install.js +207 -0
- package/dist/cli/mcp-install.js.map +1 -0
- package/dist/cli/sources.d.ts +10 -0
- package/dist/cli/sources.d.ts.map +1 -0
- package/dist/cli/sources.js +132 -0
- package/dist/cli/sources.js.map +1 -0
- package/dist/cli/status.d.ts +6 -0
- package/dist/cli/status.d.ts.map +1 -0
- package/dist/cli/status.js +93 -0
- package/dist/cli/status.js.map +1 -0
- package/dist/cli/sync.d.ts +21 -0
- package/dist/cli/sync.d.ts.map +1 -0
- package/dist/cli/sync.js +321 -0
- package/dist/cli/sync.js.map +1 -0
- package/dist/core/ports/embedding-provider.d.ts +15 -0
- package/dist/core/ports/embedding-provider.d.ts.map +1 -0
- package/dist/core/ports/embedding-provider.js +2 -0
- package/dist/core/ports/embedding-provider.js.map +1 -0
- package/dist/core/ports/git-provider.d.ts +30 -0
- package/dist/core/ports/git-provider.d.ts.map +1 -0
- package/dist/core/ports/git-provider.js +2 -0
- package/dist/core/ports/git-provider.js.map +1 -0
- package/dist/core/ports/github-provider.d.ts +30 -0
- package/dist/core/ports/github-provider.d.ts.map +1 -0
- package/dist/core/ports/github-provider.js +2 -0
- package/dist/core/ports/github-provider.js.map +1 -0
- package/dist/core/ports/index.d.ts +6 -0
- package/dist/core/ports/index.d.ts.map +1 -0
- package/dist/core/ports/index.js +2 -0
- package/dist/core/ports/index.js.map +1 -0
- package/dist/core/ports/source-provider.d.ts +27 -0
- package/dist/core/ports/source-provider.d.ts.map +1 -0
- package/dist/core/ports/source-provider.js +2 -0
- package/dist/core/ports/source-provider.js.map +1 -0
- package/dist/core/ports/storage.d.ts +47 -0
- package/dist/core/ports/storage.d.ts.map +1 -0
- package/dist/core/ports/storage.js +2 -0
- package/dist/core/ports/storage.js.map +1 -0
- package/dist/core/types/config.d.ts +26 -0
- package/dist/core/types/config.d.ts.map +1 -0
- package/dist/core/types/config.js +41 -0
- package/dist/core/types/config.js.map +1 -0
- package/dist/core/types/git.d.ts +35 -0
- package/dist/core/types/git.d.ts.map +1 -0
- package/dist/core/types/git.js +6 -0
- package/dist/core/types/git.js.map +1 -0
- package/dist/core/types/github.d.ts +75 -0
- package/dist/core/types/github.d.ts.map +1 -0
- package/dist/core/types/github.js +2 -0
- package/dist/core/types/github.js.map +1 -0
- package/dist/core/types/index.d.ts +7 -0
- package/dist/core/types/index.d.ts.map +1 -0
- package/dist/core/types/index.js +2 -0
- package/dist/core/types/index.js.map +1 -0
- package/dist/core/types/issue.d.ts +74 -0
- package/dist/core/types/issue.d.ts.map +1 -0
- package/dist/core/types/issue.js +7 -0
- package/dist/core/types/issue.js.map +1 -0
- package/dist/core/types/project.d.ts +9 -0
- package/dist/core/types/project.d.ts.map +1 -0
- package/dist/core/types/project.js +5 -0
- package/dist/core/types/project.js.map +1 -0
- package/dist/db/connection.d.ts +2 -0
- package/dist/db/connection.d.ts.map +1 -0
- package/dist/db/connection.js +4 -0
- package/dist/db/connection.js.map +1 -0
- package/dist/db/import.d.ts +2 -0
- package/dist/db/import.d.ts.map +1 -0
- package/dist/db/import.js +4 -0
- package/dist/db/import.js.map +1 -0
- package/dist/db/schema.d.ts +2 -0
- package/dist/db/schema.d.ts.map +1 -0
- package/dist/db/schema.js +4 -0
- package/dist/db/schema.js.map +1 -0
- package/dist/jira/client.d.ts +2 -0
- package/dist/jira/client.d.ts.map +1 -0
- package/dist/jira/client.js +4 -0
- package/dist/jira/client.js.map +1 -0
- package/dist/jira/pull.d.ts +2 -0
- package/dist/jira/pull.d.ts.map +1 -0
- package/dist/jira/pull.js +5 -0
- package/dist/jira/pull.js.map +1 -0
- package/dist/mcp/server.d.ts +16 -0
- package/dist/mcp/server.d.ts.map +1 -0
- package/dist/mcp/server.js +1463 -0
- package/dist/mcp/server.js.map +1 -0
- package/dist/use-cases/embed.d.ts +23 -0
- package/dist/use-cases/embed.d.ts.map +1 -0
- package/dist/use-cases/embed.js +63 -0
- package/dist/use-cases/embed.js.map +1 -0
- package/dist/use-cases/pull-git.d.ts +25 -0
- package/dist/use-cases/pull-git.d.ts.map +1 -0
- package/dist/use-cases/pull-git.js +59 -0
- package/dist/use-cases/pull-git.js.map +1 -0
- package/dist/use-cases/pull-github.d.ts +28 -0
- package/dist/use-cases/pull-github.d.ts.map +1 -0
- package/dist/use-cases/pull-github.js +67 -0
- package/dist/use-cases/pull-github.js.map +1 -0
- package/dist/use-cases/pull.d.ts +32 -0
- package/dist/use-cases/pull.d.ts.map +1 -0
- package/dist/use-cases/pull.js +82 -0
- package/dist/use-cases/pull.js.map +1 -0
- package/dist/workspace/config.d.ts +36 -0
- package/dist/workspace/config.d.ts.map +1 -0
- package/dist/workspace/config.js +91 -0
- package/dist/workspace/config.js.map +1 -0
- package/dist/workspace/resolver.d.ts +19 -0
- package/dist/workspace/resolver.d.ts.map +1 -0
- package/dist/workspace/resolver.js +47 -0
- package/dist/workspace/resolver.js.map +1 -0
- package/package.json +71 -0
- package/templates/docker-compose.yml +26 -0
- package/templates/env.example +14 -0
- package/templates/gitignore +6 -0
- package/templates/init.sql +236 -0
package/dist/cli/init.js
ADDED
|
@@ -0,0 +1,966 @@
|
|
|
1
|
+
import { input, confirm, password, checkbox } from '@inquirer/prompts';
|
|
2
|
+
import { mkdirSync, writeFileSync, copyFileSync, existsSync } from 'node:fs';
|
|
3
|
+
import { execSync, spawn } from 'node:child_process';
|
|
4
|
+
import { join, resolve } from 'node:path';
|
|
5
|
+
import { fileURLToPath } from 'node:url';
|
|
6
|
+
import chalk from 'chalk';
|
|
7
|
+
import ora from 'ora';
|
|
8
|
+
import { isWorkspace } from '../workspace/resolver.js';
|
|
9
|
+
import { createEmptyConfig, addSource, writeConfig } from '../workspace/config.js';
|
|
10
|
+
import { resolveServerPath } from './mcp-install.js';
|
|
11
|
+
import { SOURCE_META, ALL_SOURCES } from '../core/types/index.js';
|
|
12
|
+
const currentDir = fileURLToPath(new URL('.', import.meta.url));
|
|
13
|
+
function getTemplatesDir() {
|
|
14
|
+
const templatesDir = resolve(currentDir, '..', '..', 'templates');
|
|
15
|
+
if (!existsSync(templatesDir)) {
|
|
16
|
+
throw new Error(`Templates directory not found: ${templatesDir}`);
|
|
17
|
+
}
|
|
18
|
+
return templatesDir;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Strip path, query, fragment from a Jira URL.
|
|
22
|
+
* User may paste full board URL like:
|
|
23
|
+
* https://team.atlassian.net/jira/software/c/projects/PAP/boards/43?search=462
|
|
24
|
+
* We only need: https://team.atlassian.net
|
|
25
|
+
*/
|
|
26
|
+
function extractJiraBaseUrl(raw) {
|
|
27
|
+
try {
|
|
28
|
+
const url = new URL(raw.trim());
|
|
29
|
+
return `${url.protocol}//${url.host}`;
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
return raw.trim().replace(/\/+$/, '');
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
// ─── Jira connection test (shared between interactive and non-interactive) ───
|
|
36
|
+
async function testJiraConnection(url, email, token) {
|
|
37
|
+
const { Version3Client } = await import('jira.js');
|
|
38
|
+
const client = new Version3Client({
|
|
39
|
+
host: url,
|
|
40
|
+
authentication: { basic: { email, apiToken: token } },
|
|
41
|
+
});
|
|
42
|
+
const result = await client.projects.searchProjects({ maxResults: 200 });
|
|
43
|
+
return result.values.map((p) => p.key);
|
|
44
|
+
}
|
|
45
|
+
// ─── Interactive source setup ────────────────────────────────────────────────
|
|
46
|
+
async function setupJiraInteractive() {
|
|
47
|
+
console.log('');
|
|
48
|
+
console.log(chalk.bold(' Jira setup'));
|
|
49
|
+
console.log(chalk.dim(' Connect to your Jira instance.\n'));
|
|
50
|
+
const jiraUrlRaw = await input({
|
|
51
|
+
message: 'Jira URL:',
|
|
52
|
+
default: 'https://your-team.atlassian.net',
|
|
53
|
+
validate: (val) => {
|
|
54
|
+
if (!val.startsWith('https://')) {
|
|
55
|
+
return 'Must start with https://';
|
|
56
|
+
}
|
|
57
|
+
return true;
|
|
58
|
+
},
|
|
59
|
+
});
|
|
60
|
+
const jiraUrl = extractJiraBaseUrl(jiraUrlRaw);
|
|
61
|
+
const jiraEmail = await input({
|
|
62
|
+
message: 'Email:',
|
|
63
|
+
validate: (val) => {
|
|
64
|
+
if (!val.includes('@')) {
|
|
65
|
+
return 'Must be a valid email';
|
|
66
|
+
}
|
|
67
|
+
return true;
|
|
68
|
+
},
|
|
69
|
+
});
|
|
70
|
+
let jiraToken;
|
|
71
|
+
let availableProjects;
|
|
72
|
+
for (;;) {
|
|
73
|
+
jiraToken = await password({
|
|
74
|
+
message: 'API Token:',
|
|
75
|
+
mask: '*',
|
|
76
|
+
validate: (val) => {
|
|
77
|
+
if (!val.trim()) {
|
|
78
|
+
return 'Token is required';
|
|
79
|
+
}
|
|
80
|
+
return true;
|
|
81
|
+
},
|
|
82
|
+
});
|
|
83
|
+
const spinner = ora('Testing Jira connection...').start();
|
|
84
|
+
try {
|
|
85
|
+
availableProjects = await testJiraConnection(jiraUrl, jiraEmail, jiraToken);
|
|
86
|
+
}
|
|
87
|
+
catch (err) {
|
|
88
|
+
spinner.fail('Connection failed');
|
|
89
|
+
console.log(chalk.red(` Error: ${err instanceof Error ? err.message : String(err)}`));
|
|
90
|
+
console.log(chalk.dim(' Check URL, email, token. Generate token:'));
|
|
91
|
+
console.log(chalk.dim(' https://id.atlassian.com/manage-profile/security/api-tokens'));
|
|
92
|
+
const retry = await confirm({ message: 'Try again?', default: true });
|
|
93
|
+
if (retry) {
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
const skip = await confirm({ message: 'Skip Jira for now?', default: false });
|
|
97
|
+
if (skip) {
|
|
98
|
+
return null;
|
|
99
|
+
}
|
|
100
|
+
return setupJiraInteractive();
|
|
101
|
+
}
|
|
102
|
+
if (availableProjects.length === 0) {
|
|
103
|
+
spinner.warn('Connected, but found 0 projects. Token may have limited permissions.');
|
|
104
|
+
console.log(chalk.yellow(' Your token works but has no project access.'));
|
|
105
|
+
console.log(chalk.dim(' This usually means the token was pasted incorrectly or has restricted scopes.'));
|
|
106
|
+
const retry = await confirm({ message: 'Re-enter token?', default: true });
|
|
107
|
+
if (retry) {
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
else {
|
|
112
|
+
spinner.succeed(`Connected! Found ${availableProjects.length} projects: ${availableProjects.join(', ')}`);
|
|
113
|
+
}
|
|
114
|
+
break;
|
|
115
|
+
}
|
|
116
|
+
const jiraProjects = await checkbox({
|
|
117
|
+
message: 'Select projects to pull:',
|
|
118
|
+
choices: availableProjects.map((key) => ({
|
|
119
|
+
value: key,
|
|
120
|
+
name: key,
|
|
121
|
+
})),
|
|
122
|
+
});
|
|
123
|
+
if (jiraProjects.length === 0) {
|
|
124
|
+
console.log(chalk.yellow(' No projects selected.'));
|
|
125
|
+
return null;
|
|
126
|
+
}
|
|
127
|
+
return { jiraUrl, jiraEmail, jiraToken, jiraProjects };
|
|
128
|
+
}
|
|
129
|
+
function gitCloneWithProgress(url, targetPath, spinner) {
|
|
130
|
+
return new Promise((res, rej) => {
|
|
131
|
+
const proc = spawn('git', ['clone', '--progress', url, targetPath], {
|
|
132
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
133
|
+
cwd: process.env['HOME'],
|
|
134
|
+
});
|
|
135
|
+
const repoName = url.split('/').pop()?.replace(/\.git$/, '') ?? 'repo';
|
|
136
|
+
proc.stderr.on('data', (data) => {
|
|
137
|
+
const line = data.toString().trim();
|
|
138
|
+
const match = /(\w[\w\s]+?):\s+(\d+)%\s+\((\d+)\/(\d+)\)/.exec(line);
|
|
139
|
+
if (match) {
|
|
140
|
+
const [, phase, pct] = match;
|
|
141
|
+
spinner.text = `Cloning ${repoName}: ${phase?.trim()} ${pct}%`;
|
|
142
|
+
}
|
|
143
|
+
});
|
|
144
|
+
proc.on('close', (code) => {
|
|
145
|
+
if (code === 0) {
|
|
146
|
+
res();
|
|
147
|
+
}
|
|
148
|
+
else {
|
|
149
|
+
rej(new Error(`git clone exited with code ${String(code)}`));
|
|
150
|
+
}
|
|
151
|
+
});
|
|
152
|
+
proc.on('error', rej);
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
async function collectGitRepoLocal() {
|
|
156
|
+
const rawPath = await input({
|
|
157
|
+
message: 'Path to local repo:',
|
|
158
|
+
validate: (val) => {
|
|
159
|
+
const trimmed = val.trim();
|
|
160
|
+
if (!trimmed) {
|
|
161
|
+
return 'Path is required';
|
|
162
|
+
}
|
|
163
|
+
const resolved = resolve(trimmed.replace(/^~/, process.env['HOME'] ?? '~'));
|
|
164
|
+
if (!existsSync(join(resolved, '.git'))) {
|
|
165
|
+
return `Not a git repository: ${resolved} (no .git/ directory)`;
|
|
166
|
+
}
|
|
167
|
+
return true;
|
|
168
|
+
},
|
|
169
|
+
});
|
|
170
|
+
return resolve(rawPath.trim().replace(/^~/, process.env['HOME'] ?? '~'));
|
|
171
|
+
}
|
|
172
|
+
async function collectGitRepoGithub() {
|
|
173
|
+
const githubToken = await password({
|
|
174
|
+
message: 'GitHub token (PAT):',
|
|
175
|
+
mask: '*',
|
|
176
|
+
validate: (val) => {
|
|
177
|
+
if (!val.trim()) {
|
|
178
|
+
return 'Token is required';
|
|
179
|
+
}
|
|
180
|
+
return true;
|
|
181
|
+
},
|
|
182
|
+
});
|
|
183
|
+
const spinner = ora('Fetching accessible repositories...').start();
|
|
184
|
+
let repos;
|
|
185
|
+
try {
|
|
186
|
+
const { Octokit } = await import('octokit');
|
|
187
|
+
const octokit = new Octokit({ auth: githubToken.trim() });
|
|
188
|
+
const result = await octokit.rest.repos.listForAuthenticatedUser({
|
|
189
|
+
per_page: 100,
|
|
190
|
+
sort: 'updated',
|
|
191
|
+
direction: 'desc',
|
|
192
|
+
});
|
|
193
|
+
repos = result.data.map((r) => ({
|
|
194
|
+
full_name: r.full_name,
|
|
195
|
+
clone_url: r.clone_url,
|
|
196
|
+
isPrivate: r.private,
|
|
197
|
+
description: r.description,
|
|
198
|
+
}));
|
|
199
|
+
spinner.succeed(`Found ${repos.length} repositories`);
|
|
200
|
+
}
|
|
201
|
+
catch (err) {
|
|
202
|
+
spinner.fail('Failed to fetch repositories');
|
|
203
|
+
console.log(chalk.red(` Error: ${err instanceof Error ? err.message : String(err)}`));
|
|
204
|
+
return { paths: [], token: githubToken.trim(), repos: [] };
|
|
205
|
+
}
|
|
206
|
+
if (repos.length === 0) {
|
|
207
|
+
console.log(chalk.yellow(' No repositories accessible with this token.'));
|
|
208
|
+
return { paths: [], token: githubToken.trim(), repos: [] };
|
|
209
|
+
}
|
|
210
|
+
const selectedRepos = await checkbox({
|
|
211
|
+
message: 'Select repositories to clone:',
|
|
212
|
+
choices: repos.map((r) => ({
|
|
213
|
+
value: { cloneUrl: r.clone_url, fullName: r.full_name },
|
|
214
|
+
name: `${r.full_name} ${r.isPrivate ? '(private)' : '(public)'}`,
|
|
215
|
+
...(r.description ? { description: r.description } : {}),
|
|
216
|
+
})),
|
|
217
|
+
});
|
|
218
|
+
if (selectedRepos.length === 0) {
|
|
219
|
+
console.log(chalk.yellow(' No repositories selected.'));
|
|
220
|
+
return { paths: [], token: githubToken.trim(), repos: [] };
|
|
221
|
+
}
|
|
222
|
+
const clonedPaths = [];
|
|
223
|
+
const clonedRepos = [];
|
|
224
|
+
for (const { cloneUrl, fullName } of selectedRepos) {
|
|
225
|
+
const repoName = cloneUrl.split('/').pop()?.replace(/\.git$/, '') ?? 'repo';
|
|
226
|
+
const defaultPath = resolve(repoName);
|
|
227
|
+
const cloneDir = await input({
|
|
228
|
+
message: `Clone ${repoName} into directory:`,
|
|
229
|
+
default: defaultPath,
|
|
230
|
+
});
|
|
231
|
+
const targetPath = resolve(cloneDir.trim().replace(/^~/, process.env['HOME'] ?? '~'));
|
|
232
|
+
const cloneSpinner = ora(`Cloning ${repoName}...`).start();
|
|
233
|
+
try {
|
|
234
|
+
await gitCloneWithProgress(cloneUrl, targetPath, cloneSpinner);
|
|
235
|
+
cloneSpinner.succeed(`Cloned to ${targetPath}`);
|
|
236
|
+
clonedPaths.push(targetPath);
|
|
237
|
+
clonedRepos.push(fullName);
|
|
238
|
+
}
|
|
239
|
+
catch (err) {
|
|
240
|
+
cloneSpinner.fail(`Failed to clone ${repoName}`);
|
|
241
|
+
console.log(chalk.red(` Error: ${err instanceof Error ? err.message : String(err)}`));
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
return { paths: clonedPaths, token: githubToken.trim(), repos: clonedRepos };
|
|
245
|
+
}
|
|
246
|
+
async function collectGitRepoUrl() {
|
|
247
|
+
const repoUrl = await input({
|
|
248
|
+
message: 'Repository URL (HTTPS):',
|
|
249
|
+
validate: (val) => {
|
|
250
|
+
const trimmed = val.trim();
|
|
251
|
+
if (!trimmed) {
|
|
252
|
+
return 'URL is required';
|
|
253
|
+
}
|
|
254
|
+
if (!trimmed.startsWith('https://') && !trimmed.startsWith('git@')) {
|
|
255
|
+
return 'Must start with https:// or git@';
|
|
256
|
+
}
|
|
257
|
+
return true;
|
|
258
|
+
},
|
|
259
|
+
});
|
|
260
|
+
const defaultDir = repoUrl.trim().split('/').pop()?.replace(/\.git$/, '') ?? 'repo';
|
|
261
|
+
const defaultPath = resolve(defaultDir);
|
|
262
|
+
const cloneDir = await input({
|
|
263
|
+
message: 'Clone into directory:',
|
|
264
|
+
default: defaultPath,
|
|
265
|
+
});
|
|
266
|
+
const targetPath = resolve(cloneDir.trim().replace(/^~/, process.env['HOME'] ?? '~'));
|
|
267
|
+
const spinner = ora(`Cloning ${repoUrl.trim()}...`).start();
|
|
268
|
+
try {
|
|
269
|
+
await gitCloneWithProgress(repoUrl.trim(), targetPath, spinner);
|
|
270
|
+
spinner.succeed(`Cloned to ${targetPath}`);
|
|
271
|
+
return targetPath;
|
|
272
|
+
}
|
|
273
|
+
catch (err) {
|
|
274
|
+
spinner.fail('Clone failed');
|
|
275
|
+
console.log(chalk.red(` Error: ${err instanceof Error ? err.message : String(err)}`));
|
|
276
|
+
console.log(chalk.dim(' Check URL and network. Make sure git is installed.'));
|
|
277
|
+
return null;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
async function setupGitInteractive() {
|
|
281
|
+
console.log('');
|
|
282
|
+
console.log(chalk.bold(' Git setup'));
|
|
283
|
+
console.log(chalk.dim(' Connect to Git repositories.\n'));
|
|
284
|
+
const { select } = await import('@inquirer/prompts');
|
|
285
|
+
const gitRepoPaths = [];
|
|
286
|
+
let savedGithubToken;
|
|
287
|
+
const savedGithubRepos = [];
|
|
288
|
+
let addMore = true;
|
|
289
|
+
while (addMore) {
|
|
290
|
+
const mode = await select({
|
|
291
|
+
message: gitRepoPaths.length === 0
|
|
292
|
+
? 'Where is your Git repository?'
|
|
293
|
+
: 'Add another Git repository:',
|
|
294
|
+
choices: [
|
|
295
|
+
{
|
|
296
|
+
value: 'local',
|
|
297
|
+
name: 'Local path',
|
|
298
|
+
description: 'The repo is already downloaded to your computer. ' +
|
|
299
|
+
'You just point to the folder where it lives (e.g. ~/projects/my-app). ' +
|
|
300
|
+
'Nothing is downloaded — Argustack reads commit history directly from disk',
|
|
301
|
+
},
|
|
302
|
+
{
|
|
303
|
+
value: 'github',
|
|
304
|
+
name: 'Clone from GitHub',
|
|
305
|
+
description: 'You have a GitHub account with access to the repo. ' +
|
|
306
|
+
'Enter your GitHub token — Argustack will show all repos you have access to, ' +
|
|
307
|
+
'you pick the ones you need, and they download automatically',
|
|
308
|
+
},
|
|
309
|
+
{
|
|
310
|
+
value: 'clone',
|
|
311
|
+
name: 'Clone from URL',
|
|
312
|
+
description: 'You have a direct link to the repo (from GitHub, GitLab, Bitbucket, or any git server). ' +
|
|
313
|
+
'Paste the URL and Argustack will download the repo for you. ' +
|
|
314
|
+
'Use this if your repo is not on GitHub or you prefer to paste the link manually',
|
|
315
|
+
},
|
|
316
|
+
],
|
|
317
|
+
});
|
|
318
|
+
if (mode === 'local') {
|
|
319
|
+
const path = await collectGitRepoLocal();
|
|
320
|
+
if (path) {
|
|
321
|
+
gitRepoPaths.push(path);
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
else if (mode === 'github') {
|
|
325
|
+
const result = await collectGitRepoGithub();
|
|
326
|
+
gitRepoPaths.push(...result.paths);
|
|
327
|
+
savedGithubToken = result.token;
|
|
328
|
+
savedGithubRepos.push(...result.repos);
|
|
329
|
+
}
|
|
330
|
+
else {
|
|
331
|
+
const path = await collectGitRepoUrl();
|
|
332
|
+
if (path) {
|
|
333
|
+
gitRepoPaths.push(path);
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
if (gitRepoPaths.length === 0) {
|
|
337
|
+
const skip = await confirm({ message: 'Skip Git for now?', default: true });
|
|
338
|
+
if (skip) {
|
|
339
|
+
return null;
|
|
340
|
+
}
|
|
341
|
+
continue;
|
|
342
|
+
}
|
|
343
|
+
addMore = await confirm({ message: 'Add another Git repository?', default: false });
|
|
344
|
+
}
|
|
345
|
+
if (gitRepoPaths.length === 0) {
|
|
346
|
+
return null;
|
|
347
|
+
}
|
|
348
|
+
for (const p of gitRepoPaths) {
|
|
349
|
+
console.log(chalk.green(` Git source configured: ${p}`));
|
|
350
|
+
}
|
|
351
|
+
return {
|
|
352
|
+
gitRepoPaths,
|
|
353
|
+
...(savedGithubToken ? { githubToken: savedGithubToken } : {}),
|
|
354
|
+
...(savedGithubRepos.length > 0 ? { githubRepos: savedGithubRepos } : {}),
|
|
355
|
+
};
|
|
356
|
+
}
|
|
357
|
+
async function setupGithubInteractive(existingToken, existingRepos) {
|
|
358
|
+
console.log('');
|
|
359
|
+
console.log(chalk.bold(' GitHub setup'));
|
|
360
|
+
console.log(chalk.dim(' Connect to GitHub API for PRs, reviews, and releases.\n'));
|
|
361
|
+
if (existingToken && existingRepos && existingRepos.length > 0) {
|
|
362
|
+
const repoToUse = existingRepos[0] ?? '';
|
|
363
|
+
const [owner = '', repo = ''] = repoToUse.split('/');
|
|
364
|
+
if (existingRepos.length === 1) {
|
|
365
|
+
console.log(chalk.green(` Auto-configured from Git step: ${repoToUse}`));
|
|
366
|
+
}
|
|
367
|
+
else {
|
|
368
|
+
console.log(chalk.dim(` Repos from Git step: ${existingRepos.join(', ')}`));
|
|
369
|
+
console.log(chalk.green(` Using first repo for GitHub PRs: ${repoToUse}`));
|
|
370
|
+
}
|
|
371
|
+
return {
|
|
372
|
+
githubToken: existingToken.trim(),
|
|
373
|
+
githubOwner: owner,
|
|
374
|
+
githubRepo: repo,
|
|
375
|
+
};
|
|
376
|
+
}
|
|
377
|
+
let githubToken;
|
|
378
|
+
if (existingToken) {
|
|
379
|
+
console.log(chalk.green(' Using GitHub token from Git clone step.'));
|
|
380
|
+
githubToken = existingToken;
|
|
381
|
+
}
|
|
382
|
+
else {
|
|
383
|
+
console.log(chalk.dim(' Generate token: Settings → Developer settings → Personal access tokens'));
|
|
384
|
+
githubToken = await password({
|
|
385
|
+
message: 'GitHub token (PAT):',
|
|
386
|
+
mask: '*',
|
|
387
|
+
validate: (val) => {
|
|
388
|
+
if (!val.trim()) {
|
|
389
|
+
return 'Token is required';
|
|
390
|
+
}
|
|
391
|
+
return true;
|
|
392
|
+
},
|
|
393
|
+
});
|
|
394
|
+
}
|
|
395
|
+
const spinner = ora('Fetching accessible repositories...').start();
|
|
396
|
+
let repos;
|
|
397
|
+
try {
|
|
398
|
+
const { Octokit } = await import('octokit');
|
|
399
|
+
const octokit = new Octokit({ auth: githubToken.trim() });
|
|
400
|
+
const result = await octokit.rest.repos.listForAuthenticatedUser({
|
|
401
|
+
per_page: 100,
|
|
402
|
+
sort: 'updated',
|
|
403
|
+
direction: 'desc',
|
|
404
|
+
});
|
|
405
|
+
repos = result.data.map((r) => ({
|
|
406
|
+
full_name: r.full_name,
|
|
407
|
+
isPrivate: r.private,
|
|
408
|
+
description: r.description,
|
|
409
|
+
}));
|
|
410
|
+
spinner.succeed(`Found ${repos.length} accessible repositories`);
|
|
411
|
+
}
|
|
412
|
+
catch (err) {
|
|
413
|
+
spinner.fail('Failed to fetch repositories');
|
|
414
|
+
console.log(chalk.red(` Error: ${err instanceof Error ? err.message : String(err)}`));
|
|
415
|
+
console.log(chalk.dim(' You can add GitHub token to .env later.'));
|
|
416
|
+
const skip = await confirm({ message: 'Skip GitHub for now?', default: true });
|
|
417
|
+
if (skip) {
|
|
418
|
+
return null;
|
|
419
|
+
}
|
|
420
|
+
return setupGithubInteractive(existingToken);
|
|
421
|
+
}
|
|
422
|
+
if (repos.length === 0) {
|
|
423
|
+
console.log(chalk.yellow(' No repositories accessible with this token.'));
|
|
424
|
+
console.log(chalk.dim(' Make sure the token has access to at least one repository.'));
|
|
425
|
+
return null;
|
|
426
|
+
}
|
|
427
|
+
const { select } = await import('@inquirer/prompts');
|
|
428
|
+
const selectedRepo = await select({
|
|
429
|
+
message: 'Select repository:',
|
|
430
|
+
choices: repos.map((r) => ({
|
|
431
|
+
value: r.full_name,
|
|
432
|
+
name: `${r.full_name} ${r.isPrivate ? '(private)' : '(public)'}`,
|
|
433
|
+
...(r.description ? { description: r.description } : {}),
|
|
434
|
+
})),
|
|
435
|
+
});
|
|
436
|
+
const [owner = '', repo = ''] = selectedRepo.split('/');
|
|
437
|
+
console.log(chalk.green(` GitHub configured: ${selectedRepo}`));
|
|
438
|
+
return {
|
|
439
|
+
githubToken: githubToken.trim(),
|
|
440
|
+
githubOwner: owner,
|
|
441
|
+
githubRepo: repo,
|
|
442
|
+
};
|
|
443
|
+
}
|
|
444
|
+
async function setupCsvInteractive() {
|
|
445
|
+
console.log('');
|
|
446
|
+
console.log(chalk.bold(' Jira CSV Import setup'));
|
|
447
|
+
console.log(chalk.dim(' Import issues from a Jira CSV export file.\n'));
|
|
448
|
+
const csvFilePath = await input({
|
|
449
|
+
message: 'Path to Jira CSV file:',
|
|
450
|
+
validate: (val) => {
|
|
451
|
+
if (!val.trim()) {
|
|
452
|
+
return 'CSV file path is required';
|
|
453
|
+
}
|
|
454
|
+
return true;
|
|
455
|
+
},
|
|
456
|
+
});
|
|
457
|
+
const resolved = resolve(csvFilePath.replace(/^~/, process.env['HOME'] ?? '~'));
|
|
458
|
+
console.log(chalk.green(` CSV file: ${resolved}`));
|
|
459
|
+
return { csvFilePath: resolved };
|
|
460
|
+
}
|
|
461
|
+
async function setupDbInteractive() {
|
|
462
|
+
console.log('');
|
|
463
|
+
console.log(chalk.bold(' Database setup'));
|
|
464
|
+
console.log(chalk.dim(' Connect to the project database you want to analyze.\n'));
|
|
465
|
+
console.log(chalk.dim(' (This is the TARGET database, not Argustack internal DB)\n'));
|
|
466
|
+
const targetDbHost = await input({ message: 'DB Host:', default: 'localhost' });
|
|
467
|
+
const targetDbPortStr = await input({
|
|
468
|
+
message: 'DB Port:', default: '5432',
|
|
469
|
+
validate: (val) => {
|
|
470
|
+
const n = parseInt(val, 10);
|
|
471
|
+
if (isNaN(n) || n < 1 || n > 65535) {
|
|
472
|
+
return 'Port must be 1-65535';
|
|
473
|
+
}
|
|
474
|
+
return true;
|
|
475
|
+
},
|
|
476
|
+
});
|
|
477
|
+
const targetDbUser = await input({ message: 'DB User:' });
|
|
478
|
+
const targetDbPassword = await password({ message: 'DB Password:' });
|
|
479
|
+
const targetDbName = await input({ message: 'DB Name:' });
|
|
480
|
+
const targetDbPort = parseInt(targetDbPortStr, 10);
|
|
481
|
+
console.log(chalk.green(` Database configured: ${targetDbUser}@${targetDbHost}:${targetDbPort}/${targetDbName}`));
|
|
482
|
+
return { targetDbHost, targetDbPort, targetDbUser, targetDbPassword, targetDbName };
|
|
483
|
+
}
|
|
484
|
+
// ─── Non-interactive source setup (from flags) ──────────────────────────────
|
|
485
|
+
async function setupJiraFromFlags(flags) {
|
|
486
|
+
if (!flags.jiraUrl || !flags.jiraEmail || !flags.jiraToken) {
|
|
487
|
+
throw new Error('Jira requires: --jira-url, --jira-email, --jira-token');
|
|
488
|
+
}
|
|
489
|
+
const spinner = ora('Testing Jira connection...').start();
|
|
490
|
+
try {
|
|
491
|
+
const availableProjects = await testJiraConnection(extractJiraBaseUrl(flags.jiraUrl), flags.jiraEmail, flags.jiraToken);
|
|
492
|
+
spinner.succeed(`Connected! Found ${availableProjects.length} projects: ${availableProjects.join(', ')}`);
|
|
493
|
+
let jiraProjects;
|
|
494
|
+
if (!flags.jiraProjects || flags.jiraProjects.toLowerCase() === 'all') {
|
|
495
|
+
jiraProjects = availableProjects;
|
|
496
|
+
}
|
|
497
|
+
else {
|
|
498
|
+
jiraProjects = flags.jiraProjects.split(',').map((p) => p.trim().toUpperCase());
|
|
499
|
+
}
|
|
500
|
+
return {
|
|
501
|
+
jiraUrl: flags.jiraUrl,
|
|
502
|
+
jiraEmail: flags.jiraEmail,
|
|
503
|
+
jiraToken: flags.jiraToken,
|
|
504
|
+
jiraProjects,
|
|
505
|
+
};
|
|
506
|
+
}
|
|
507
|
+
catch (err) {
|
|
508
|
+
spinner.fail('Connection failed');
|
|
509
|
+
throw new Error(`Jira connection failed: ${err instanceof Error ? err.message : String(err)}`, { cause: err });
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
function setupGitFromFlags(flags) {
|
|
513
|
+
if (!flags.gitRepo) {
|
|
514
|
+
throw new Error('Git requires: --git-repo');
|
|
515
|
+
}
|
|
516
|
+
const paths = flags.gitRepo.split(',').map((p) => p.trim()).filter(Boolean);
|
|
517
|
+
return { gitRepoPaths: paths };
|
|
518
|
+
}
|
|
519
|
+
function setupGithubFromFlags(flags) {
|
|
520
|
+
if (!flags.githubToken || !flags.githubOwner || !flags.githubRepo) {
|
|
521
|
+
throw new Error('GitHub requires: --github-token, --github-owner, --github-repo');
|
|
522
|
+
}
|
|
523
|
+
return {
|
|
524
|
+
githubToken: flags.githubToken,
|
|
525
|
+
githubOwner: flags.githubOwner,
|
|
526
|
+
githubRepo: flags.githubRepo,
|
|
527
|
+
};
|
|
528
|
+
}
|
|
529
|
+
function setupDbFromFlags(flags) {
|
|
530
|
+
if (!flags.targetDbHost || !flags.targetDbUser || !flags.targetDbName) {
|
|
531
|
+
throw new Error('Database requires: --target-db-host, --target-db-user, --target-db-name');
|
|
532
|
+
}
|
|
533
|
+
return {
|
|
534
|
+
targetDbHost: flags.targetDbHost,
|
|
535
|
+
targetDbPort: parseInt(flags.targetDbPort ?? '5432', 10),
|
|
536
|
+
targetDbUser: flags.targetDbUser,
|
|
537
|
+
targetDbPassword: flags.targetDbPassword ?? '',
|
|
538
|
+
targetDbName: flags.targetDbName,
|
|
539
|
+
};
|
|
540
|
+
}
|
|
541
|
+
function setupCsvFromFlags(flags) {
|
|
542
|
+
if (!flags.csvFile) {
|
|
543
|
+
throw new Error('CSV requires: --csv-file');
|
|
544
|
+
}
|
|
545
|
+
return { csvFilePath: flags.csvFile };
|
|
546
|
+
}
|
|
547
|
+
// ─── .env generation ─────────────────────────────────────────────────────────
|
|
548
|
+
function generateEnv(jira, git, github, csv, db, argustackDbPort) {
|
|
549
|
+
const lines = [];
|
|
550
|
+
if (jira) {
|
|
551
|
+
lines.push('# === Jira ===', `JIRA_URL=${jira.jiraUrl}`, `JIRA_EMAIL=${jira.jiraEmail}`, `JIRA_API_TOKEN=${jira.jiraToken}`, `JIRA_PROJECTS=${jira.jiraProjects.join(',')}`, '');
|
|
552
|
+
}
|
|
553
|
+
if (git) {
|
|
554
|
+
lines.push('# === Git ===', `GIT_REPO_PATHS=${git.gitRepoPaths.join(',')}`, '');
|
|
555
|
+
}
|
|
556
|
+
if (github) {
|
|
557
|
+
lines.push('# === GitHub ===', `GITHUB_TOKEN=${github.githubToken}`, `GITHUB_OWNER=${github.githubOwner}`, `GITHUB_REPO=${github.githubRepo}`, '');
|
|
558
|
+
}
|
|
559
|
+
if (csv) {
|
|
560
|
+
lines.push('# === Jira CSV ===', `CSV_FILE_PATH=${csv.csvFilePath}`, '');
|
|
561
|
+
}
|
|
562
|
+
if (db) {
|
|
563
|
+
lines.push('# === Target Database (project DB to analyze) ===', `TARGET_DB_HOST=${db.targetDbHost}`, `TARGET_DB_PORT=${db.targetDbPort}`, `TARGET_DB_USER=${db.targetDbUser}`, `TARGET_DB_PASSWORD=${db.targetDbPassword}`, `TARGET_DB_NAME=${db.targetDbName}`, '');
|
|
564
|
+
}
|
|
565
|
+
lines.push('# === Argustack internal PostgreSQL (match docker-compose.yml) ===', 'DB_HOST=localhost', `DB_PORT=${argustackDbPort}`, 'DB_USER=argustack', 'DB_PASSWORD=argustack_local', 'DB_NAME=argustack', '', '# === OpenAI embeddings (optional, for semantic search) ===', '# OPENAI_API_KEY=sk-...');
|
|
566
|
+
return lines.join('\n') + '\n';
|
|
567
|
+
}
|
|
568
|
+
// ─── docker-compose generation ───────────────────────────────────────────────
|
|
569
|
+
function generateDockerCompose(dbPort, pgwebPort) {
|
|
570
|
+
return `services:
|
|
571
|
+
db:
|
|
572
|
+
image: pgvector/pgvector:pg16
|
|
573
|
+
container_name: argustack-db
|
|
574
|
+
ports:
|
|
575
|
+
- "${dbPort}:5432"
|
|
576
|
+
environment:
|
|
577
|
+
POSTGRES_USER: argustack
|
|
578
|
+
POSTGRES_PASSWORD: argustack_local
|
|
579
|
+
POSTGRES_DB: argustack
|
|
580
|
+
volumes:
|
|
581
|
+
- argustack-data:/var/lib/postgresql/data
|
|
582
|
+
- ./db/init.sql:/docker-entrypoint-initdb.d/init.sql
|
|
583
|
+
healthcheck:
|
|
584
|
+
test: ["CMD-SHELL", "pg_isready -U argustack"]
|
|
585
|
+
interval: 2s
|
|
586
|
+
timeout: 5s
|
|
587
|
+
retries: 15
|
|
588
|
+
|
|
589
|
+
pgweb:
|
|
590
|
+
image: sosedoff/pgweb
|
|
591
|
+
container_name: argustack-pgweb
|
|
592
|
+
ports:
|
|
593
|
+
- "${pgwebPort}:8081"
|
|
594
|
+
environment:
|
|
595
|
+
PGWEB_DATABASE_URL: postgres://argustack:argustack_local@db:5432/argustack?sslmode=disable
|
|
596
|
+
depends_on:
|
|
597
|
+
db:
|
|
598
|
+
condition: service_healthy
|
|
599
|
+
restart: on-failure
|
|
600
|
+
|
|
601
|
+
volumes:
|
|
602
|
+
argustack-data:
|
|
603
|
+
`;
|
|
604
|
+
}
|
|
605
|
+
// ─── Create workspace (shared logic) ─────────────────────────────────────────
|
|
606
|
+
function createWorkspaceFiles(workspaceDir, jira, git, github, csv, db, dbPort, pgwebPort) {
|
|
607
|
+
const templatesDir = getTemplatesDir();
|
|
608
|
+
// Directories
|
|
609
|
+
mkdirSync(workspaceDir, { recursive: true });
|
|
610
|
+
mkdirSync(join(workspaceDir, '.argustack'), { recursive: true });
|
|
611
|
+
mkdirSync(join(workspaceDir, 'db'), { recursive: true });
|
|
612
|
+
mkdirSync(join(workspaceDir, 'data'), { recursive: true });
|
|
613
|
+
// Config
|
|
614
|
+
let config = createEmptyConfig();
|
|
615
|
+
if (jira) {
|
|
616
|
+
config = addSource(config, 'jira');
|
|
617
|
+
}
|
|
618
|
+
if (git) {
|
|
619
|
+
config = addSource(config, 'git');
|
|
620
|
+
}
|
|
621
|
+
if (github) {
|
|
622
|
+
config = addSource(config, 'github');
|
|
623
|
+
}
|
|
624
|
+
if (csv) {
|
|
625
|
+
config = addSource(config, 'csv');
|
|
626
|
+
}
|
|
627
|
+
if (db) {
|
|
628
|
+
config = addSource(config, 'db');
|
|
629
|
+
}
|
|
630
|
+
writeConfig(workspaceDir, config);
|
|
631
|
+
// .env
|
|
632
|
+
writeFileSync(join(workspaceDir, '.env'), generateEnv(jira, git, github, csv, db, dbPort));
|
|
633
|
+
// docker-compose.yml
|
|
634
|
+
writeFileSync(join(workspaceDir, 'docker-compose.yml'), generateDockerCompose(dbPort, pgwebPort));
|
|
635
|
+
// init.sql
|
|
636
|
+
copyFileSync(join(templatesDir, 'init.sql'), join(workspaceDir, 'db', 'init.sql'));
|
|
637
|
+
// .gitignore
|
|
638
|
+
copyFileSync(join(templatesDir, 'gitignore'), join(workspaceDir, '.gitignore'));
|
|
639
|
+
// .mcp.json — Claude Code auto-discovers MCP servers from this file
|
|
640
|
+
try {
|
|
641
|
+
const serverPath = resolveServerPath();
|
|
642
|
+
const mcpConfig = {
|
|
643
|
+
mcpServers: {
|
|
644
|
+
argustack: {
|
|
645
|
+
command: 'node',
|
|
646
|
+
args: [serverPath],
|
|
647
|
+
},
|
|
648
|
+
},
|
|
649
|
+
};
|
|
650
|
+
writeFileSync(join(workspaceDir, '.mcp.json'), JSON.stringify(mcpConfig, null, 2) + '\n');
|
|
651
|
+
}
|
|
652
|
+
catch {
|
|
653
|
+
// Server not built yet — skip .mcp.json, user can run `argustack mcp install` later
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
// ─── Summary output ──────────────────────────────────────────────────────────
|
|
657
|
+
function printSummary(workspaceDir, jira, git, github, csv, db, pgwebPort, willAutoStart) {
|
|
658
|
+
console.log('');
|
|
659
|
+
console.log(chalk.green.bold(' Done! Your workspace is ready.'));
|
|
660
|
+
console.log('');
|
|
661
|
+
console.log(chalk.dim(' Sources configured:'));
|
|
662
|
+
if (jira) {
|
|
663
|
+
console.log(` ${chalk.green('✓')} Jira — ${jira.jiraUrl}`);
|
|
664
|
+
}
|
|
665
|
+
if (git) {
|
|
666
|
+
for (const p of git.gitRepoPaths) {
|
|
667
|
+
console.log(` ${chalk.green('✓')} Git — ${p}`);
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
if (github) {
|
|
671
|
+
console.log(` ${chalk.green('✓')} GitHub — ${github.githubOwner}/${github.githubRepo}`);
|
|
672
|
+
}
|
|
673
|
+
if (csv) {
|
|
674
|
+
console.log(` ${chalk.green('✓')} Jira CSV — ${csv.csvFilePath}`);
|
|
675
|
+
}
|
|
676
|
+
if (db) {
|
|
677
|
+
console.log(` ${chalk.green('✓')} Database — ${db.targetDbHost}:${db.targetDbPort}`);
|
|
678
|
+
}
|
|
679
|
+
if (!jira && !git && !github && !csv && !db) {
|
|
680
|
+
console.log(` ${chalk.yellow('—')} None yet. Use ${chalk.cyan('argustack source add <type>')}`);
|
|
681
|
+
}
|
|
682
|
+
if (!willAutoStart) {
|
|
683
|
+
console.log('');
|
|
684
|
+
console.log(chalk.dim(' Next steps:'));
|
|
685
|
+
console.log(` ${chalk.cyan('cd')} ${workspaceDir}`);
|
|
686
|
+
console.log(` ${chalk.cyan('docker compose up -d')} # start database`);
|
|
687
|
+
if (jira) {
|
|
688
|
+
console.log(` ${chalk.cyan('argustack sync jira')} # sync from Jira`);
|
|
689
|
+
}
|
|
690
|
+
console.log(` ${chalk.cyan(`http://localhost:${pgwebPort}`)} # browse data in pgweb`);
|
|
691
|
+
console.log('');
|
|
692
|
+
console.log(chalk.dim(' Claude integration:'));
|
|
693
|
+
console.log(` Claude Code: ${chalk.green('Open this folder — MCP tools ready!')}`);
|
|
694
|
+
console.log(` Claude Desktop: ${chalk.cyan('argustack mcp install')}`);
|
|
695
|
+
}
|
|
696
|
+
console.log('');
|
|
697
|
+
}
|
|
698
|
+
// ─── Non-interactive init ────────────────────────────────────────────────────
|
|
699
|
+
async function runInitNonInteractive(flags) {
|
|
700
|
+
console.log(chalk.bold('\n Argustack — non-interactive setup\n'));
|
|
701
|
+
// Workspace dir
|
|
702
|
+
const workspaceDir = resolve((flags.dir ?? process.cwd()).replace(/^~/, process.env['HOME'] ?? '~'));
|
|
703
|
+
// Parse sources
|
|
704
|
+
const selectedSources = flags.source
|
|
705
|
+
? flags.source.split(',').map((s) => s.trim().toLowerCase())
|
|
706
|
+
: [];
|
|
707
|
+
// Validate source names
|
|
708
|
+
for (const s of selectedSources) {
|
|
709
|
+
if (!ALL_SOURCES.includes(s)) {
|
|
710
|
+
throw new Error(`Unknown source: ${s}. Available: ${ALL_SOURCES.join(', ')}`);
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
// Setup each source from flags
|
|
714
|
+
let jiraResult = null;
|
|
715
|
+
let gitResult = null;
|
|
716
|
+
let githubResult = null;
|
|
717
|
+
let csvResult = null;
|
|
718
|
+
let dbResult = null;
|
|
719
|
+
for (const source of selectedSources) {
|
|
720
|
+
switch (source) {
|
|
721
|
+
case 'jira':
|
|
722
|
+
jiraResult = await setupJiraFromFlags(flags);
|
|
723
|
+
break;
|
|
724
|
+
case 'git':
|
|
725
|
+
gitResult = setupGitFromFlags(flags);
|
|
726
|
+
break;
|
|
727
|
+
case 'github':
|
|
728
|
+
githubResult = setupGithubFromFlags(flags);
|
|
729
|
+
break;
|
|
730
|
+
case 'csv':
|
|
731
|
+
csvResult = setupCsvFromFlags(flags);
|
|
732
|
+
break;
|
|
733
|
+
case 'db':
|
|
734
|
+
dbResult = setupDbFromFlags(flags);
|
|
735
|
+
break;
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
const dbPort = parseInt(flags.dbPort ?? '5434', 10);
|
|
739
|
+
const pgwebPort = parseInt(flags.pgwebPort ?? '8086', 10);
|
|
740
|
+
// Create workspace
|
|
741
|
+
const spinner = ora('Creating workspace...').start();
|
|
742
|
+
try {
|
|
743
|
+
createWorkspaceFiles(workspaceDir, jiraResult, gitResult, githubResult, csvResult, dbResult, dbPort, pgwebPort);
|
|
744
|
+
spinner.succeed('Workspace created!');
|
|
745
|
+
}
|
|
746
|
+
catch (err) {
|
|
747
|
+
spinner.fail('Failed');
|
|
748
|
+
throw err;
|
|
749
|
+
}
|
|
750
|
+
printSummary(workspaceDir, jiraResult, gitResult, githubResult, csvResult, dbResult, pgwebPort, false);
|
|
751
|
+
}
|
|
752
|
+
// ─── Interactive init ────────────────────────────────────────────────────────
|
|
753
|
+
async function runInitInteractive(flags) {
|
|
754
|
+
console.log('');
|
|
755
|
+
console.log(chalk.bold(' Argustack — workspace setup'));
|
|
756
|
+
console.log(chalk.dim(' Cross-reference Jira + Git + DB to analyze your project.\n'));
|
|
757
|
+
// 1. Where to create?
|
|
758
|
+
const targetDir = await input({
|
|
759
|
+
message: 'Workspace directory:',
|
|
760
|
+
default: flags.dir ?? process.cwd(),
|
|
761
|
+
validate: (val) => {
|
|
762
|
+
if (!val.trim()) {
|
|
763
|
+
return 'Directory path is required';
|
|
764
|
+
}
|
|
765
|
+
return true;
|
|
766
|
+
},
|
|
767
|
+
});
|
|
768
|
+
const workspaceDir = resolve(targetDir.replace(/^~/, process.env['HOME'] ?? '~'));
|
|
769
|
+
if (isWorkspace(workspaceDir)) {
|
|
770
|
+
console.log(chalk.yellow(`\n Already an Argustack workspace: ${workspaceDir}`));
|
|
771
|
+
const proceed = await confirm({ message: 'Reinitialize this workspace?', default: false });
|
|
772
|
+
if (!proceed) {
|
|
773
|
+
console.log(chalk.dim(' Cancelled.'));
|
|
774
|
+
return;
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
// 2. Which sources?
|
|
778
|
+
console.log('');
|
|
779
|
+
const selectedSources = await checkbox({
|
|
780
|
+
message: 'Which sources do you have access to?',
|
|
781
|
+
choices: ALL_SOURCES.map((s) => ({
|
|
782
|
+
value: s,
|
|
783
|
+
name: SOURCE_META[s].label,
|
|
784
|
+
description: SOURCE_META[s].description,
|
|
785
|
+
})),
|
|
786
|
+
});
|
|
787
|
+
if (selectedSources.length === 0) {
|
|
788
|
+
console.log(chalk.yellow('\n No sources selected. You can add them later with:'));
|
|
789
|
+
console.log(chalk.cyan(' argustack source add jira'));
|
|
790
|
+
console.log(chalk.cyan(' argustack source add git'));
|
|
791
|
+
console.log(chalk.cyan(' argustack source add github'));
|
|
792
|
+
console.log(chalk.cyan(' argustack source add db'));
|
|
793
|
+
const continueAnyway = await confirm({
|
|
794
|
+
message: 'Create workspace without sources?',
|
|
795
|
+
default: true,
|
|
796
|
+
});
|
|
797
|
+
if (!continueAnyway) {
|
|
798
|
+
console.log(chalk.dim(' Cancelled.'));
|
|
799
|
+
return;
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
// 3. Collect credentials for each selected source
|
|
803
|
+
let jiraResult = null;
|
|
804
|
+
let gitResult = null;
|
|
805
|
+
let githubResult = null;
|
|
806
|
+
let csvResult = null;
|
|
807
|
+
let dbResult = null;
|
|
808
|
+
for (const source of selectedSources) {
|
|
809
|
+
switch (source) {
|
|
810
|
+
case 'jira':
|
|
811
|
+
jiraResult = await setupJiraInteractive();
|
|
812
|
+
break;
|
|
813
|
+
case 'git':
|
|
814
|
+
gitResult = await setupGitInteractive();
|
|
815
|
+
break;
|
|
816
|
+
case 'github':
|
|
817
|
+
githubResult = await setupGithubInteractive(gitResult?.githubToken, gitResult?.githubRepos);
|
|
818
|
+
break;
|
|
819
|
+
case 'csv':
|
|
820
|
+
csvResult = await setupCsvInteractive();
|
|
821
|
+
break;
|
|
822
|
+
case 'db':
|
|
823
|
+
dbResult = await setupDbInteractive();
|
|
824
|
+
break;
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
// 4. Argustack internal DB ports
|
|
828
|
+
console.log('');
|
|
829
|
+
console.log(chalk.dim(' Argustack internal database (Docker):'));
|
|
830
|
+
const dbPortStr = await input({
|
|
831
|
+
message: 'PostgreSQL port:', default: flags.dbPort ?? '5434',
|
|
832
|
+
validate: (val) => {
|
|
833
|
+
const n = parseInt(val, 10);
|
|
834
|
+
if (isNaN(n) || n < 1024 || n > 65535) {
|
|
835
|
+
return 'Port must be 1024-65535';
|
|
836
|
+
}
|
|
837
|
+
return true;
|
|
838
|
+
},
|
|
839
|
+
});
|
|
840
|
+
const pgwebPortStr = await input({
|
|
841
|
+
message: 'pgweb UI port:', default: flags.pgwebPort ?? '8086',
|
|
842
|
+
validate: (val) => {
|
|
843
|
+
const n = parseInt(val, 10);
|
|
844
|
+
if (isNaN(n) || n < 1024 || n > 65535) {
|
|
845
|
+
return 'Port must be 1024-65535';
|
|
846
|
+
}
|
|
847
|
+
return true;
|
|
848
|
+
},
|
|
849
|
+
});
|
|
850
|
+
const dbPort = parseInt(dbPortStr, 10);
|
|
851
|
+
const pgwebPort = parseInt(pgwebPortStr, 10);
|
|
852
|
+
// 5. Create workspace
|
|
853
|
+
const spinner = ora('Creating workspace...').start();
|
|
854
|
+
try {
|
|
855
|
+
createWorkspaceFiles(workspaceDir, jiraResult, gitResult, githubResult, csvResult, dbResult, dbPort, pgwebPort);
|
|
856
|
+
spinner.succeed('Workspace created!');
|
|
857
|
+
}
|
|
858
|
+
catch (err) {
|
|
859
|
+
spinner.fail('Failed to create workspace');
|
|
860
|
+
console.log(chalk.red(`\n Error: ${err instanceof Error ? err.message : String(err)}`));
|
|
861
|
+
return;
|
|
862
|
+
}
|
|
863
|
+
// 6. Offer to start DB + sync automatically
|
|
864
|
+
const autoStart = await confirm({
|
|
865
|
+
message: 'Start database and sync now?',
|
|
866
|
+
default: true,
|
|
867
|
+
});
|
|
868
|
+
printSummary(workspaceDir, jiraResult, gitResult, githubResult, csvResult, dbResult, pgwebPort, autoStart);
|
|
869
|
+
if (autoStart) {
|
|
870
|
+
await startAndSync(workspaceDir, jiraResult !== null, gitResult !== null, githubResult !== null, csvResult, pgwebPort);
|
|
871
|
+
}
|
|
872
|
+
}
|
|
873
|
+
async function startAndSync(workspaceDir, hasJira, hasGit, hasGithub, csv, pgwebPort) {
|
|
874
|
+
const spinnerDb = ora('Starting Docker containers...').start();
|
|
875
|
+
try {
|
|
876
|
+
execSync('docker compose up -d', { cwd: workspaceDir, stdio: 'pipe' });
|
|
877
|
+
spinnerDb.succeed('Database running!');
|
|
878
|
+
}
|
|
879
|
+
catch (err) {
|
|
880
|
+
spinnerDb.fail('Failed to start Docker');
|
|
881
|
+
console.log(chalk.red(` Error: ${err instanceof Error ? err.message : String(err)}`));
|
|
882
|
+
console.log(chalk.dim(' Make sure Docker Desktop is running, then:'));
|
|
883
|
+
console.log(chalk.cyan(` cd ${workspaceDir} && docker compose up -d`));
|
|
884
|
+
return;
|
|
885
|
+
}
|
|
886
|
+
// Wait for PostgreSQL to be ready
|
|
887
|
+
const spinnerWait = ora('Waiting for PostgreSQL...').start();
|
|
888
|
+
const maxWait = 30;
|
|
889
|
+
for (let i = 0; i < maxWait; i++) {
|
|
890
|
+
try {
|
|
891
|
+
execSync('docker compose exec -T db pg_isready -U argustack', { cwd: workspaceDir, stdio: 'pipe' });
|
|
892
|
+
spinnerWait.succeed('PostgreSQL ready!');
|
|
893
|
+
break;
|
|
894
|
+
}
|
|
895
|
+
catch {
|
|
896
|
+
if (i === maxWait - 1) {
|
|
897
|
+
spinnerWait.fail('PostgreSQL not ready after 30s');
|
|
898
|
+
console.log(chalk.dim(' Try manually: docker compose logs db'));
|
|
899
|
+
return;
|
|
900
|
+
}
|
|
901
|
+
await new Promise((r) => setTimeout(r, 1000));
|
|
902
|
+
}
|
|
903
|
+
}
|
|
904
|
+
if (hasJira) {
|
|
905
|
+
console.log('');
|
|
906
|
+
try {
|
|
907
|
+
const { syncJiraFromInit } = await import('./sync.js');
|
|
908
|
+
await syncJiraFromInit(workspaceDir);
|
|
909
|
+
}
|
|
910
|
+
catch (err) {
|
|
911
|
+
console.log(chalk.red(` Jira sync failed: ${err instanceof Error ? err.message : String(err)}`));
|
|
912
|
+
console.log(chalk.dim(` Try manually: cd ${workspaceDir} && argustack sync jira`));
|
|
913
|
+
}
|
|
914
|
+
}
|
|
915
|
+
if (hasGit) {
|
|
916
|
+
console.log('');
|
|
917
|
+
try {
|
|
918
|
+
const { syncGitFromInit } = await import('./sync.js');
|
|
919
|
+
await syncGitFromInit(workspaceDir);
|
|
920
|
+
}
|
|
921
|
+
catch (err) {
|
|
922
|
+
console.log(chalk.red(` Git sync failed: ${err instanceof Error ? err.message : String(err)}`));
|
|
923
|
+
console.log(chalk.dim(` Try manually: cd ${workspaceDir} && argustack sync git`));
|
|
924
|
+
}
|
|
925
|
+
}
|
|
926
|
+
if (hasGithub) {
|
|
927
|
+
console.log('');
|
|
928
|
+
try {
|
|
929
|
+
const { syncGithubFromInit } = await import('./sync.js');
|
|
930
|
+
await syncGithubFromInit(workspaceDir);
|
|
931
|
+
}
|
|
932
|
+
catch (err) {
|
|
933
|
+
console.log(chalk.red(` GitHub sync failed: ${err instanceof Error ? err.message : String(err)}`));
|
|
934
|
+
console.log(chalk.dim(` Try manually: cd ${workspaceDir} && argustack sync github`));
|
|
935
|
+
}
|
|
936
|
+
}
|
|
937
|
+
if (csv) {
|
|
938
|
+
console.log('');
|
|
939
|
+
try {
|
|
940
|
+
const { syncCsvFromInit } = await import('./sync.js');
|
|
941
|
+
await syncCsvFromInit(workspaceDir, csv.csvFilePath);
|
|
942
|
+
}
|
|
943
|
+
catch (err) {
|
|
944
|
+
console.log(chalk.red(` CSV import failed: ${err instanceof Error ? err.message : String(err)}`));
|
|
945
|
+
console.log(chalk.dim(` Try manually: cd ${workspaceDir} && argustack sync csv`));
|
|
946
|
+
}
|
|
947
|
+
}
|
|
948
|
+
console.log(chalk.dim(' What\'s next:'));
|
|
949
|
+
console.log(` ${chalk.cyan(`http://localhost:${pgwebPort}`)} # browse data in pgweb`);
|
|
950
|
+
console.log('');
|
|
951
|
+
console.log(chalk.dim(' Claude integration:'));
|
|
952
|
+
console.log(` Claude Code: ${chalk.green('Open this folder — MCP tools ready!')}`);
|
|
953
|
+
console.log(` Claude Desktop: ${chalk.cyan('argustack mcp install')}`);
|
|
954
|
+
console.log('');
|
|
955
|
+
}
|
|
956
|
+
// ─── Main entry point ────────────────────────────────────────────────────────
|
|
957
|
+
export async function runInit(flags = {}) {
|
|
958
|
+
// --no-interactive sets flags.interactive to false
|
|
959
|
+
if (flags.interactive === false) {
|
|
960
|
+
await runInitNonInteractive(flags);
|
|
961
|
+
}
|
|
962
|
+
else {
|
|
963
|
+
await runInitInteractive(flags);
|
|
964
|
+
}
|
|
965
|
+
}
|
|
966
|
+
//# sourceMappingURL=init.js.map
|