ikanban-web 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +65 -0
- package/bin/cli.js +1055 -0
- package/dist/apple-touch-icon-120x120.png +0 -0
- package/dist/apple-touch-icon-152x152.png +0 -0
- package/dist/apple-touch-icon-167x167.png +0 -0
- package/dist/apple-touch-icon-180x180.png +0 -0
- package/dist/apple-touch-icon.png +0 -0
- package/dist/apple-touch-icon.svg +16 -0
- package/dist/assets/KaTeX_AMS-Regular-BQhdFMY1.woff2 +0 -0
- package/dist/assets/KaTeX_AMS-Regular-DMm9YOAa.woff +0 -0
- package/dist/assets/KaTeX_AMS-Regular-DRggAlZN.ttf +0 -0
- package/dist/assets/KaTeX_Caligraphic-Bold-ATXxdsX0.ttf +0 -0
- package/dist/assets/KaTeX_Caligraphic-Bold-BEiXGLvX.woff +0 -0
- package/dist/assets/KaTeX_Caligraphic-Bold-Dq_IR9rO.woff2 +0 -0
- package/dist/assets/KaTeX_Caligraphic-Regular-CTRA-rTL.woff +0 -0
- package/dist/assets/KaTeX_Caligraphic-Regular-Di6jR-x-.woff2 +0 -0
- package/dist/assets/KaTeX_Caligraphic-Regular-wX97UBjC.ttf +0 -0
- package/dist/assets/KaTeX_Fraktur-Bold-BdnERNNW.ttf +0 -0
- package/dist/assets/KaTeX_Fraktur-Bold-BsDP51OF.woff +0 -0
- package/dist/assets/KaTeX_Fraktur-Bold-CL6g_b3V.woff2 +0 -0
- package/dist/assets/KaTeX_Fraktur-Regular-CB_wures.ttf +0 -0
- package/dist/assets/KaTeX_Fraktur-Regular-CTYiF6lA.woff2 +0 -0
- package/dist/assets/KaTeX_Fraktur-Regular-Dxdc4cR9.woff +0 -0
- package/dist/assets/KaTeX_Main-Bold-Cx986IdX.woff2 +0 -0
- package/dist/assets/KaTeX_Main-Bold-Jm3AIy58.woff +0 -0
- package/dist/assets/KaTeX_Main-Bold-waoOVXN0.ttf +0 -0
- package/dist/assets/KaTeX_Main-BoldItalic-DxDJ3AOS.woff2 +0 -0
- package/dist/assets/KaTeX_Main-BoldItalic-DzxPMmG6.ttf +0 -0
- package/dist/assets/KaTeX_Main-BoldItalic-SpSLRI95.woff +0 -0
- package/dist/assets/KaTeX_Main-Italic-3WenGoN9.ttf +0 -0
- package/dist/assets/KaTeX_Main-Italic-BMLOBm91.woff +0 -0
- package/dist/assets/KaTeX_Main-Italic-NWA7e6Wa.woff2 +0 -0
- package/dist/assets/KaTeX_Main-Regular-B22Nviop.woff2 +0 -0
- package/dist/assets/KaTeX_Main-Regular-Dr94JaBh.woff +0 -0
- package/dist/assets/KaTeX_Main-Regular-ypZvNtVU.ttf +0 -0
- package/dist/assets/KaTeX_Math-BoldItalic-B3XSjfu4.ttf +0 -0
- package/dist/assets/KaTeX_Math-BoldItalic-CZnvNsCZ.woff2 +0 -0
- package/dist/assets/KaTeX_Math-BoldItalic-iY-2wyZ7.woff +0 -0
- package/dist/assets/KaTeX_Math-Italic-DA0__PXp.woff +0 -0
- package/dist/assets/KaTeX_Math-Italic-flOr_0UB.ttf +0 -0
- package/dist/assets/KaTeX_Math-Italic-t53AETM-.woff2 +0 -0
- package/dist/assets/KaTeX_SansSerif-Bold-CFMepnvq.ttf +0 -0
- package/dist/assets/KaTeX_SansSerif-Bold-D1sUS0GD.woff2 +0 -0
- package/dist/assets/KaTeX_SansSerif-Bold-DbIhKOiC.woff +0 -0
- package/dist/assets/KaTeX_SansSerif-Italic-C3H0VqGB.woff2 +0 -0
- package/dist/assets/KaTeX_SansSerif-Italic-DN2j7dab.woff +0 -0
- package/dist/assets/KaTeX_SansSerif-Italic-YYjJ1zSn.ttf +0 -0
- package/dist/assets/KaTeX_SansSerif-Regular-BNo7hRIc.ttf +0 -0
- package/dist/assets/KaTeX_SansSerif-Regular-CS6fqUqJ.woff +0 -0
- package/dist/assets/KaTeX_SansSerif-Regular-DDBCnlJ7.woff2 +0 -0
- package/dist/assets/KaTeX_Script-Regular-C5JkGWo-.ttf +0 -0
- package/dist/assets/KaTeX_Script-Regular-D3wIWfF6.woff2 +0 -0
- package/dist/assets/KaTeX_Script-Regular-D5yQViql.woff +0 -0
- package/dist/assets/KaTeX_Size1-Regular-C195tn64.woff +0 -0
- package/dist/assets/KaTeX_Size1-Regular-Dbsnue_I.ttf +0 -0
- package/dist/assets/KaTeX_Size1-Regular-mCD8mA8B.woff2 +0 -0
- package/dist/assets/KaTeX_Size2-Regular-B7gKUWhC.ttf +0 -0
- package/dist/assets/KaTeX_Size2-Regular-Dy4dx90m.woff2 +0 -0
- package/dist/assets/KaTeX_Size2-Regular-oD1tc_U0.woff +0 -0
- package/dist/assets/KaTeX_Size3-Regular-CTq5MqoE.woff +0 -0
- package/dist/assets/KaTeX_Size3-Regular-DgpXs0kz.ttf +0 -0
- package/dist/assets/KaTeX_Size4-Regular-BF-4gkZK.woff +0 -0
- package/dist/assets/KaTeX_Size4-Regular-DWFBv043.ttf +0 -0
- package/dist/assets/KaTeX_Size4-Regular-Dl5lxZxV.woff2 +0 -0
- package/dist/assets/KaTeX_Typewriter-Regular-C0xS9mPB.woff +0 -0
- package/dist/assets/KaTeX_Typewriter-Regular-CO6r4hn1.woff2 +0 -0
- package/dist/assets/KaTeX_Typewriter-Regular-D3Ib7_Hf.ttf +0 -0
- package/dist/assets/ToolOutputDialog-BgFj3J4u.js +5 -0
- package/dist/assets/ibm-plex-mono-latin-400-normal-CvHOgSBP.woff +0 -0
- package/dist/assets/ibm-plex-mono-latin-400-normal-DMJ8VG8y.woff2 +0 -0
- package/dist/assets/ibm-plex-mono-latin-500-normal-CB9ihrfo.woff +0 -0
- package/dist/assets/ibm-plex-mono-latin-500-normal-DSY6xOcd.woff2 +0 -0
- package/dist/assets/ibm-plex-mono-latin-600-normal-BgSNZQsw.woff2 +0 -0
- package/dist/assets/ibm-plex-mono-latin-600-normal-DWFSQ4vo.woff +0 -0
- package/dist/assets/ibm-plex-sans-latin-400-normal-CDDApCn2.woff2 +0 -0
- package/dist/assets/ibm-plex-sans-latin-400-normal-CYLoc0-x.woff +0 -0
- package/dist/assets/ibm-plex-sans-latin-500-normal-6ng42L7E.woff2 +0 -0
- package/dist/assets/ibm-plex-sans-latin-500-normal-BgVn5rGT.woff +0 -0
- package/dist/assets/ibm-plex-sans-latin-600-normal-Cu4Hd6ag.woff +0 -0
- package/dist/assets/ibm-plex-sans-latin-600-normal-CuJfVYMP.woff2 +0 -0
- package/dist/assets/index-BQJPAP5f.css +1 -0
- package/dist/assets/index-MBuF9PXk.js +2 -0
- package/dist/assets/main-C1GggQif.css +1 -0
- package/dist/assets/main-Dyy8WvPJ.js +311 -0
- package/dist/assets/vendor--DEobTDja.css +1 -0
- package/dist/assets/vendor-.bun-KwPUu5oc.js +4594 -0
- package/dist/assets/wasm-CG6Dc4jp.js +1 -0
- package/dist/assets/worker-suRQxAtO.js +155 -0
- package/dist/favicon-16.png +0 -0
- package/dist/favicon-32.png +0 -0
- package/dist/favicon.png +0 -0
- package/dist/favicon.svg +15 -0
- package/dist/index.html +325 -0
- package/dist/logo-dark-192x192.png +0 -0
- package/dist/logo-dark-512x512.svg +16 -0
- package/dist/logo-light-192x192.png +0 -0
- package/dist/logo-light-512x512.svg +16 -0
- package/dist/pwa-192.png +0 -0
- package/dist/pwa-512.png +0 -0
- package/dist/pwa-maskable-192.png +0 -0
- package/dist/pwa-maskable-512.png +0 -0
- package/dist/site.webmanifest +22 -0
- package/dist/sw.js +1 -0
- package/package.json +102 -0
- package/public/apple-touch-icon-120x120.png +0 -0
- package/public/apple-touch-icon-152x152.png +0 -0
- package/public/apple-touch-icon-167x167.png +0 -0
- package/public/apple-touch-icon-180x180.png +0 -0
- package/public/apple-touch-icon.png +0 -0
- package/public/apple-touch-icon.svg +16 -0
- package/public/favicon-16.png +0 -0
- package/public/favicon-32.png +0 -0
- package/public/favicon.png +0 -0
- package/public/favicon.svg +15 -0
- package/public/logo-dark-192x192.png +0 -0
- package/public/logo-dark-512x512.svg +16 -0
- package/public/logo-light-192x192.png +0 -0
- package/public/logo-light-512x512.svg +16 -0
- package/public/pwa-192.png +0 -0
- package/public/pwa-512.png +0 -0
- package/public/pwa-maskable-192.png +0 -0
- package/public/pwa-maskable-512.png +0 -0
- package/public/site.webmanifest +22 -0
- package/server/TERMINAL_INPUT_WS_PROTOCOL.md +44 -0
- package/server/index.d.ts +28 -0
- package/server/index.js +11181 -0
- package/server/lib/cloudflare-tunnel.js +196 -0
- package/server/lib/git-credentials.js +74 -0
- package/server/lib/git-identity-storage.js +110 -0
- package/server/lib/git-service.js +2690 -0
- package/server/lib/github-auth.js +307 -0
- package/server/lib/github-device-flow.js +50 -0
- package/server/lib/github-octokit.js +10 -0
- package/server/lib/github-repo.js +55 -0
- package/server/lib/notification-message.js +49 -0
- package/server/lib/notification-message.test.js +59 -0
- package/server/lib/opencode-auth.js +81 -0
- package/server/lib/opencode-config.js +1840 -0
- package/server/lib/opencode-config.js.d.ts +12 -0
- package/server/lib/package-manager.js +255 -0
- package/server/lib/quota/DOCUMENTATION.md +52 -0
- package/server/lib/quota/index.js +21 -0
- package/server/lib/quota/providers/claude.js +107 -0
- package/server/lib/quota/providers/codex.js +111 -0
- package/server/lib/quota/providers/copilot.js +165 -0
- package/server/lib/quota/providers/google/api.js +92 -0
- package/server/lib/quota/providers/google/auth.js +108 -0
- package/server/lib/quota/providers/google/index.js +124 -0
- package/server/lib/quota/providers/google/transforms.js +109 -0
- package/server/lib/quota/providers/index.js +128 -0
- package/server/lib/quota/providers/interface.js +55 -0
- package/server/lib/quota/providers/kimi.js +108 -0
- package/server/lib/quota/providers/nanogpt.js +124 -0
- package/server/lib/quota/providers/openai.js +91 -0
- package/server/lib/quota/providers/openrouter.js +92 -0
- package/server/lib/quota/providers/zai.js +91 -0
- package/server/lib/quota/utils/auth.js +46 -0
- package/server/lib/quota/utils/formatters.js +76 -0
- package/server/lib/quota/utils/index.js +10 -0
- package/server/lib/quota/utils/transformers.js +55 -0
- package/server/lib/skills-catalog/cache.js +29 -0
- package/server/lib/skills-catalog/clawdhub/api.js +158 -0
- package/server/lib/skills-catalog/clawdhub/index.js +30 -0
- package/server/lib/skills-catalog/clawdhub/install.js +223 -0
- package/server/lib/skills-catalog/clawdhub/scan.js +113 -0
- package/server/lib/skills-catalog/curated-sources.js +21 -0
- package/server/lib/skills-catalog/git.js +76 -0
- package/server/lib/skills-catalog/install.js +280 -0
- package/server/lib/skills-catalog/scan.js +221 -0
- package/server/lib/skills-catalog/source.js +85 -0
- package/server/lib/summarization-service.js +171 -0
- package/server/lib/terminal-input-ws-protocol.js +66 -0
- package/server/lib/terminal-input-ws-protocol.test.js +138 -0
- package/server/lib/tts-service.js +162 -0
- package/server/lib/ui-auth.js +518 -0
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
import { spawn, spawnSync } from 'child_process';
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
import os from 'os';
|
|
4
|
+
import path from 'path';
|
|
5
|
+
import { fileURLToPath } from 'url';
|
|
6
|
+
|
|
7
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
8
|
+
const __dirname = path.dirname(__filename);
|
|
9
|
+
|
|
10
|
+
const TRY_CF_URL_REGEX = /https:\/\/[a-z0-9-]+\.trycloudflare\.com/i;
|
|
11
|
+
|
|
12
|
+
async function searchPathFor(command) {
|
|
13
|
+
const pathValue = process.env.PATH || '';
|
|
14
|
+
const segments = pathValue.split(path.delimiter).filter(Boolean);
|
|
15
|
+
const WINDOWS_EXTENSIONS = process.platform === 'win32'
|
|
16
|
+
? (process.env.PATHEXT || '.EXE;.CMD;.BAT;.COM')
|
|
17
|
+
.split(';')
|
|
18
|
+
.map((ext) => ext.trim().toLowerCase())
|
|
19
|
+
.filter(Boolean)
|
|
20
|
+
.map((ext) => (ext.startsWith('.') ? ext : `.${ext}`))
|
|
21
|
+
: [''];
|
|
22
|
+
|
|
23
|
+
for (const dir of segments) {
|
|
24
|
+
for (const ext of WINDOWS_EXTENSIONS) {
|
|
25
|
+
const fileName = process.platform === 'win32' ? `${command}${ext}` : command;
|
|
26
|
+
const candidate = path.join(dir, fileName);
|
|
27
|
+
try {
|
|
28
|
+
const stats = fs.statSync(candidate);
|
|
29
|
+
if (stats.isFile()) {
|
|
30
|
+
if (process.platform !== 'win32') {
|
|
31
|
+
try {
|
|
32
|
+
fs.accessSync(candidate, fs.constants.X_OK);
|
|
33
|
+
} catch {
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return candidate;
|
|
38
|
+
}
|
|
39
|
+
} catch {
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export async function checkCloudflaredAvailable() {
|
|
48
|
+
const cfPath = await searchPathFor('cloudflared');
|
|
49
|
+
if (cfPath) {
|
|
50
|
+
try {
|
|
51
|
+
const result = spawnSync(cfPath, ['--version'], { encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] });
|
|
52
|
+
if (result.status === 0) {
|
|
53
|
+
return { available: true, path: cfPath, version: result.stdout.trim() };
|
|
54
|
+
}
|
|
55
|
+
} catch {
|
|
56
|
+
// Ignore
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return { available: false, path: null, version: null };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function printCloudflareTunnelInstallHelp() {
|
|
63
|
+
const platform = process.platform;
|
|
64
|
+
let installCmd = '';
|
|
65
|
+
|
|
66
|
+
if (platform === 'darwin') {
|
|
67
|
+
installCmd = 'brew install cloudflared';
|
|
68
|
+
} else if (platform === 'win32') {
|
|
69
|
+
installCmd = 'winget install --id Cloudflare.cloudflared';
|
|
70
|
+
} else {
|
|
71
|
+
installCmd = 'Download from https://github.com/cloudflare/cloudflared/releases';
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
console.log(`
|
|
75
|
+
╔══════════════════════════════════════════════════════════════════╗
|
|
76
|
+
║ Cloudflare tunnel requires 'cloudflared' to be installed ║
|
|
77
|
+
╚══════════════════════════════════════════════════════════════════╝
|
|
78
|
+
|
|
79
|
+
Install instructions for your platform:
|
|
80
|
+
|
|
81
|
+
macOS: brew install cloudflared
|
|
82
|
+
Windows: winget install --id Cloudflare.cloudflared
|
|
83
|
+
Linux: Download from https://github.com/cloudflare/cloudflared/releases
|
|
84
|
+
|
|
85
|
+
Or visit: https://developers.cloudflare.com/cloudflare-one/networks/connectors/cloudflared/downloads/
|
|
86
|
+
`);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export async function startCloudflareTunnel({ originUrl, port }) {
|
|
90
|
+
const cfCheck = await checkCloudflaredAvailable();
|
|
91
|
+
|
|
92
|
+
if (!cfCheck.available) {
|
|
93
|
+
printCloudflareTunnelInstallHelp();
|
|
94
|
+
throw new Error('cloudflared is not installed');
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
console.log(`Using cloudflared: ${cfCheck.path} (${cfCheck.version})`);
|
|
98
|
+
|
|
99
|
+
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ikanban-cf-'));
|
|
100
|
+
|
|
101
|
+
const child = spawn('cloudflared', ['tunnel', '--url', originUrl], {
|
|
102
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
103
|
+
env: {
|
|
104
|
+
...process.env,
|
|
105
|
+
HOME: tempDir,
|
|
106
|
+
CF_TELEMETRY_DISABLE: '1',
|
|
107
|
+
},
|
|
108
|
+
killSignal: 'SIGINT',
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
let publicUrl = null;
|
|
112
|
+
let tunnelReady = false;
|
|
113
|
+
|
|
114
|
+
const onData = (chunk, isStderr) => {
|
|
115
|
+
const text = chunk.toString('utf8');
|
|
116
|
+
|
|
117
|
+
if (!tunnelReady) {
|
|
118
|
+
const match = text.match(TRY_CF_URL_REGEX);
|
|
119
|
+
if (match) {
|
|
120
|
+
publicUrl = match[0];
|
|
121
|
+
tunnelReady = true;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
process.stderr.write(isStderr ? text : '');
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
child.stdout.on('data', (chunk) => onData(chunk, false));
|
|
129
|
+
child.stderr.on('data', (chunk) => onData(chunk, true));
|
|
130
|
+
|
|
131
|
+
child.on('error', (error) => {
|
|
132
|
+
console.error(`Cloudflared error: ${error.message}`);
|
|
133
|
+
cleanupTempDir();
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
const cleanupTempDir = () => {
|
|
137
|
+
try {
|
|
138
|
+
if (fs.existsSync(tempDir)) {
|
|
139
|
+
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
140
|
+
}
|
|
141
|
+
} catch {
|
|
142
|
+
// Ignore cleanup errors
|
|
143
|
+
}
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
await new Promise((resolve, reject) => {
|
|
147
|
+
const timeout = setTimeout(() => {
|
|
148
|
+
if (!publicUrl) {
|
|
149
|
+
reject(new Error('Tunnel URL not received within 30 seconds'));
|
|
150
|
+
}
|
|
151
|
+
}, 30000);
|
|
152
|
+
|
|
153
|
+
const checkReady = setInterval(() => {
|
|
154
|
+
if (publicUrl) {
|
|
155
|
+
clearTimeout(timeout);
|
|
156
|
+
clearInterval(checkReady);
|
|
157
|
+
resolve(null);
|
|
158
|
+
}
|
|
159
|
+
}, 100);
|
|
160
|
+
|
|
161
|
+
child.on('exit', (code) => {
|
|
162
|
+
clearTimeout(timeout);
|
|
163
|
+
clearInterval(checkReady);
|
|
164
|
+
cleanupTempDir();
|
|
165
|
+
if (code !== null && code !== 0) {
|
|
166
|
+
reject(new Error(`Cloudflared exited with code ${code}`));
|
|
167
|
+
}
|
|
168
|
+
});
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
return {
|
|
172
|
+
stop: () => {
|
|
173
|
+
try {
|
|
174
|
+
child.kill('SIGINT');
|
|
175
|
+
} catch {
|
|
176
|
+
// Ignore
|
|
177
|
+
}
|
|
178
|
+
},
|
|
179
|
+
process: child,
|
|
180
|
+
getPublicUrl: () => publicUrl,
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
export function printTunnelWarning() {
|
|
185
|
+
console.log(`
|
|
186
|
+
⚠️ Cloudflare Quick Tunnel Limitations:
|
|
187
|
+
|
|
188
|
+
• Maximum 200 concurrent requests
|
|
189
|
+
• Server-Sent Events (SSE) are NOT supported
|
|
190
|
+
• URLs are temporary and will expire when the tunnel stops
|
|
191
|
+
• Password protection is required for tunnel access
|
|
192
|
+
|
|
193
|
+
For production use, set up a named Cloudflare Tunnel:
|
|
194
|
+
https://developers.cloudflare.com/cloudflare-one/networks/connectors/cloudflare-tunnel/
|
|
195
|
+
`);
|
|
196
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import os from 'os';
|
|
4
|
+
|
|
5
|
+
const GIT_CREDENTIALS_PATH = path.join(os.homedir(), '.git-credentials');
|
|
6
|
+
|
|
7
|
+
export function discoverGitCredentials() {
|
|
8
|
+
const credentials = [];
|
|
9
|
+
|
|
10
|
+
if (!fs.existsSync(GIT_CREDENTIALS_PATH)) {
|
|
11
|
+
return credentials;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
try {
|
|
15
|
+
const content = fs.readFileSync(GIT_CREDENTIALS_PATH, 'utf8');
|
|
16
|
+
const lines = content.split('\n').filter(line => line.trim());
|
|
17
|
+
|
|
18
|
+
for (const line of lines) {
|
|
19
|
+
try {
|
|
20
|
+
const url = new URL(line.trim());
|
|
21
|
+
const hostname = url.hostname;
|
|
22
|
+
const pathname = url.pathname && url.pathname !== '/' ? url.pathname : '';
|
|
23
|
+
const host = hostname + pathname;
|
|
24
|
+
const username = url.username || '';
|
|
25
|
+
|
|
26
|
+
if (host && username) {
|
|
27
|
+
const exists = credentials.some(c => c.host === host && c.username === username);
|
|
28
|
+
if (!exists) {
|
|
29
|
+
credentials.push({ host, username });
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
} catch {
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
} catch (error) {
|
|
37
|
+
console.error('Failed to read .git-credentials:', error);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return credentials;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function getCredentialForHost(host) {
|
|
44
|
+
if (!fs.existsSync(GIT_CREDENTIALS_PATH)) {
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
try {
|
|
49
|
+
const content = fs.readFileSync(GIT_CREDENTIALS_PATH, 'utf8');
|
|
50
|
+
const lines = content.split('\n').filter(line => line.trim());
|
|
51
|
+
|
|
52
|
+
for (const line of lines) {
|
|
53
|
+
try {
|
|
54
|
+
const url = new URL(line.trim());
|
|
55
|
+
const hostname = url.hostname;
|
|
56
|
+
const pathname = url.pathname && url.pathname !== '/' ? url.pathname : '';
|
|
57
|
+
const credHost = hostname + pathname;
|
|
58
|
+
|
|
59
|
+
if (credHost === host) {
|
|
60
|
+
return {
|
|
61
|
+
username: url.username || '',
|
|
62
|
+
token: url.password || ''
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
} catch {
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
} catch (error) {
|
|
70
|
+
console.error('Failed to read .git-credentials for host lookup:', error);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import os from 'os';
|
|
4
|
+
|
|
5
|
+
const STORAGE_DIR = path.join(os.homedir(), '.config', 'ikanban');
|
|
6
|
+
const STORAGE_FILE = path.join(STORAGE_DIR, 'git-identities.json');
|
|
7
|
+
|
|
8
|
+
function ensureStorageDir() {
|
|
9
|
+
if (!fs.existsSync(STORAGE_DIR)) {
|
|
10
|
+
fs.mkdirSync(STORAGE_DIR, { recursive: true });
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function loadProfiles() {
|
|
15
|
+
ensureStorageDir();
|
|
16
|
+
|
|
17
|
+
if (!fs.existsSync(STORAGE_FILE)) {
|
|
18
|
+
return { profiles: [] };
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
try {
|
|
22
|
+
const content = fs.readFileSync(STORAGE_FILE, 'utf8');
|
|
23
|
+
const data = JSON.parse(content);
|
|
24
|
+
return data;
|
|
25
|
+
} catch (error) {
|
|
26
|
+
console.error('Failed to load git identity profiles:', error);
|
|
27
|
+
return { profiles: [] };
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function saveProfiles(data) {
|
|
32
|
+
ensureStorageDir();
|
|
33
|
+
|
|
34
|
+
try {
|
|
35
|
+
fs.writeFileSync(STORAGE_FILE, JSON.stringify(data, null, 2), 'utf8');
|
|
36
|
+
return true;
|
|
37
|
+
} catch (error) {
|
|
38
|
+
console.error('Failed to save git identity profiles:', error);
|
|
39
|
+
throw error;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function getProfiles() {
|
|
44
|
+
const data = loadProfiles();
|
|
45
|
+
return data.profiles || [];
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function getProfile(id) {
|
|
49
|
+
const profiles = getProfiles();
|
|
50
|
+
return profiles.find(p => p.id === id) || null;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function createProfile(profileData) {
|
|
54
|
+
const profiles = getProfiles();
|
|
55
|
+
|
|
56
|
+
if (profiles.some(p => p.id === profileData.id)) {
|
|
57
|
+
throw new Error(`Profile with ID "${profileData.id}" already exists`);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (!profileData.id || !profileData.userName || !profileData.userEmail) {
|
|
61
|
+
throw new Error('Profile must have id, userName, and userEmail');
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const newProfile = {
|
|
65
|
+
id: profileData.id,
|
|
66
|
+
name: profileData.name || profileData.userName,
|
|
67
|
+
userName: profileData.userName,
|
|
68
|
+
userEmail: profileData.userEmail,
|
|
69
|
+
authType: profileData.authType || 'ssh',
|
|
70
|
+
sshKey: profileData.sshKey || null,
|
|
71
|
+
host: profileData.host || null,
|
|
72
|
+
color: profileData.color || 'keyword',
|
|
73
|
+
icon: profileData.icon || 'branch'
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
profiles.push(newProfile);
|
|
77
|
+
saveProfiles({ profiles });
|
|
78
|
+
|
|
79
|
+
return newProfile;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function updateProfile(id, updates) {
|
|
83
|
+
const profiles = getProfiles();
|
|
84
|
+
const index = profiles.findIndex(p => p.id === id);
|
|
85
|
+
|
|
86
|
+
if (index === -1) {
|
|
87
|
+
throw new Error(`Profile with ID "${id}" not found`);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
profiles[index] = {
|
|
91
|
+
...profiles[index],
|
|
92
|
+
...updates,
|
|
93
|
+
id: profiles[index].id
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
saveProfiles({ profiles });
|
|
97
|
+
return profiles[index];
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export function deleteProfile(id) {
|
|
101
|
+
const profiles = getProfiles();
|
|
102
|
+
const filteredProfiles = profiles.filter(p => p.id !== id);
|
|
103
|
+
|
|
104
|
+
if (filteredProfiles.length === profiles.length) {
|
|
105
|
+
throw new Error(`Profile with ID "${id}" not found`);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
saveProfiles({ profiles: filteredProfiles });
|
|
109
|
+
return true;
|
|
110
|
+
}
|