@algochad/archcoder 2.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +113 -0
- package/bin/cli-entry.js +55 -0
- package/bin/cli-output.js +145 -0
- package/bin/cli.js +5108 -0
- package/bin/cli.test.js +56 -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 +67 -0
- package/dist/assets/MultiRunWindow-BZp3MjJP.js +1 -0
- package/dist/assets/SettingsWindow-DoGYXpX7.js +1 -0
- package/dist/assets/TerminalView-BN7BR5Ff.js +3 -0
- package/dist/assets/TimelineDialog-ZQ33oVQR.js +1 -0
- package/dist/assets/ToolOutputDialog-Blv3pnug.js +16 -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-CtCEGYrr.css +1 -0
- package/dist/assets/index-o_d2wtWC.js +48 -0
- package/dist/assets/main-5QGBtzdq.css +1 -0
- package/dist/assets/main-B6oiMU86.js +8033 -0
- package/dist/assets/vendor--DbVqbJpV.css +1 -0
- package/dist/assets/vendor-.bun-HTKwyaEM.js +10086 -0
- package/dist/assets/wasm-CG6Dc4jp.js +1 -0
- package/dist/assets/worker-bqd4RMrj.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 +67 -0
- package/dist/index.html +533 -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 +107 -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 +67 -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 +67 -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 +37 -0
- package/server/index.js +14694 -0
- package/server/lib/cloudflare-tunnel.js +650 -0
- package/server/lib/git/DOCUMENTATION.md +146 -0
- package/server/lib/git/credentials.js +74 -0
- package/server/lib/git/identity-storage.js +110 -0
- package/server/lib/git/index.js +6 -0
- package/server/lib/git/service.js +3117 -0
- package/server/lib/github/DOCUMENTATION.md +170 -0
- package/server/lib/github/auth.js +307 -0
- package/server/lib/github/device-flow.js +50 -0
- package/server/lib/github/index.js +24 -0
- package/server/lib/github/octokit.js +10 -0
- package/server/lib/github/pr-status.js +478 -0
- package/server/lib/github/repo/index.js +55 -0
- package/server/lib/installer/desktop.js +289 -0
- package/server/lib/installer/download.js +208 -0
- package/server/lib/installer/index.js +45 -0
- package/server/lib/installer/platform.js +100 -0
- package/server/lib/notifications/DOCUMENTATION.md +61 -0
- package/server/lib/notifications/index.js +1 -0
- package/server/lib/notifications/message.js +49 -0
- package/server/lib/notifications/message.test.js +59 -0
- package/server/lib/opencode/DOCUMENTATION.md +59 -0
- package/server/lib/opencode/agents.js +634 -0
- package/server/lib/opencode/auth.js +81 -0
- package/server/lib/opencode/commands.js +339 -0
- package/server/lib/opencode/index.js +66 -0
- package/server/lib/opencode/mcp.js +206 -0
- package/server/lib/opencode/providers.js +96 -0
- package/server/lib/opencode/shared.js +527 -0
- package/server/lib/opencode/skills.js +480 -0
- package/server/lib/opencode/tunnel-auth.js +591 -0
- package/server/lib/opencode/ui-auth.js +510 -0
- package/server/lib/package-manager.js +505 -0
- package/server/lib/quota/DOCUMENTATION.md +55 -0
- package/server/lib/quota/index.js +24 -0
- package/server/lib/quota/providers/claude.js +107 -0
- package/server/lib/quota/providers/codex.js +113 -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 +152 -0
- package/server/lib/quota/providers/interface.js +55 -0
- package/server/lib/quota/providers/kimi.js +108 -0
- package/server/lib/quota/providers/minimax-cn-coding-plan.js +15 -0
- package/server/lib/quota/providers/minimax-coding-plan.js +15 -0
- package/server/lib/quota/providers/minimax-shared.js +136 -0
- package/server/lib/quota/providers/nanogpt.js +124 -0
- package/server/lib/quota/providers/ollama-cloud.js +112 -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/DOCUMENTATION.md +178 -0
- package/server/lib/skills-catalog/cache.js +32 -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 +238 -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 +77 -0
- package/server/lib/skills-catalog/index.js +42 -0
- package/server/lib/skills-catalog/install.js +294 -0
- package/server/lib/skills-catalog/scan.js +221 -0
- package/server/lib/skills-catalog/source.js +85 -0
- package/server/lib/terminal/DOCUMENTATION.md +114 -0
- package/server/lib/terminal/index.js +12 -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/DOCUMENTATION.md +134 -0
- package/server/lib/tts/index.js +16 -0
- package/server/lib/tts/service.js +162 -0
- package/server/lib/tts/summarization.js +171 -0
- package/server/lib/tunnels/index.js +166 -0
- package/server/lib/tunnels/providers/cloudflare.js +260 -0
- package/server/lib/tunnels/registry.js +51 -0
- package/server/lib/tunnels/types.js +219 -0
- package/server/lib/utils/lru.js +107 -0
- package/server/lib/utils/sse.js +121 -0
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import os from 'os';
|
|
4
|
+
import { spawn } from 'child_process';
|
|
5
|
+
import { getInstallDir } from './platform.js';
|
|
6
|
+
|
|
7
|
+
const APP_NAME = 'ArchCoder';
|
|
8
|
+
const APP_ID = 'ai.opencode.archcoder';
|
|
9
|
+
|
|
10
|
+
async function installDesktopIntegration(artifactPath, version, options) {
|
|
11
|
+
const platform = process.platform;
|
|
12
|
+
|
|
13
|
+
if (platform === 'linux') {
|
|
14
|
+
return installLinuxIntegration(artifactPath, version, options);
|
|
15
|
+
} else if (platform === 'darwin') {
|
|
16
|
+
return installMacOSIntegration(artifactPath, version, options);
|
|
17
|
+
} else if (platform === 'win32') {
|
|
18
|
+
return installWindowsIntegration(artifactPath, version, options);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
throw new Error(`Unsupported platform: ${platform}`);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
async function uninstallDesktopIntegration(options) {
|
|
25
|
+
const platform = process.platform;
|
|
26
|
+
|
|
27
|
+
if (platform === 'linux') {
|
|
28
|
+
return uninstallLinuxIntegration(options);
|
|
29
|
+
} else if (platform === 'darwin') {
|
|
30
|
+
return uninstallMacOSIntegration(options);
|
|
31
|
+
} else if (platform === 'win32') {
|
|
32
|
+
return uninstallWindowsIntegration(options);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
throw new Error(`Unsupported platform: ${platform}`);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async function getDesktopStatus() {
|
|
39
|
+
const platform = process.platform;
|
|
40
|
+
const installDir = getInstallDir();
|
|
41
|
+
const result = {
|
|
42
|
+
desktopEntry: null,
|
|
43
|
+
appPath: null,
|
|
44
|
+
shortcuts: [],
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
if (platform === 'linux') {
|
|
48
|
+
const desktopFile = path.join(os.homedir(), '.local', 'share', 'applications', `${APP_NAME.toLowerCase()}.desktop`);
|
|
49
|
+
if (fs.existsSync(desktopFile)) {
|
|
50
|
+
result.desktopEntry = desktopFile;
|
|
51
|
+
}
|
|
52
|
+
const appImagePath = path.join(installDir, 'ArchCoder.AppImage');
|
|
53
|
+
if (fs.existsSync(appImagePath)) {
|
|
54
|
+
result.appPath = appImagePath;
|
|
55
|
+
}
|
|
56
|
+
} else if (platform === 'darwin') {
|
|
57
|
+
const appBundlePath = path.join('/Applications', `${APP_NAME}.app`);
|
|
58
|
+
if (fs.existsSync(appBundlePath)) {
|
|
59
|
+
result.appPath = appBundlePath;
|
|
60
|
+
}
|
|
61
|
+
} else if (platform === 'win32') {
|
|
62
|
+
const appExePath = path.join(installDir, `${APP_NAME}.exe`);
|
|
63
|
+
if (fs.existsSync(appExePath)) {
|
|
64
|
+
result.appPath = appExePath;
|
|
65
|
+
}
|
|
66
|
+
const startMenuDir = path.join(process.env.APPDATA || '', 'Microsoft', 'Windows', 'Start Menu', 'Programs', APP_NAME);
|
|
67
|
+
if (fs.existsSync(startMenuDir)) {
|
|
68
|
+
result.shortcuts.push(startMenuDir);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return result;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
async function runCommand(command, args, options = {}) {
|
|
76
|
+
return new Promise((resolve, reject) => {
|
|
77
|
+
const proc = spawn(command, args, {
|
|
78
|
+
...options,
|
|
79
|
+
stdio: options.silent ? 'pipe' : 'inherit',
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
let stdout = '';
|
|
83
|
+
let stderr = '';
|
|
84
|
+
|
|
85
|
+
if (options.silent) {
|
|
86
|
+
proc.stdout?.on('data', data => stdout += data);
|
|
87
|
+
proc.stderr?.on('data', data => stderr += data);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
proc.on('close', code => {
|
|
91
|
+
if (code === 0) {
|
|
92
|
+
resolve({ stdout, stderr });
|
|
93
|
+
} else {
|
|
94
|
+
reject(new Error(`${command} exited with code ${code}: ${stderr}`));
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
proc.on('error', reject);
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
async function installLinuxIntegration(artifactPath, version, options) {
|
|
102
|
+
const installDir = getInstallDir();
|
|
103
|
+
fs.mkdirSync(installDir, { recursive: true });
|
|
104
|
+
|
|
105
|
+
const appImagePath = path.join(installDir, 'ArchCoder.AppImage');
|
|
106
|
+
|
|
107
|
+
if (fs.existsSync(appImagePath)) {
|
|
108
|
+
fs.unlinkSync(appImagePath);
|
|
109
|
+
}
|
|
110
|
+
fs.cpSync(artifactPath, appImagePath);
|
|
111
|
+
fs.chmodSync(appImagePath, 0o755);
|
|
112
|
+
|
|
113
|
+
const applicationsDir = path.join(os.homedir(), '.local', 'share', 'applications');
|
|
114
|
+
fs.mkdirSync(applicationsDir, { recursive: true });
|
|
115
|
+
|
|
116
|
+
const desktopContent = `[Desktop Entry]
|
|
117
|
+
Version=1.0
|
|
118
|
+
Name=${APP_NAME}
|
|
119
|
+
Comment=AI-powered coding assistant
|
|
120
|
+
Exec=${appImagePath}
|
|
121
|
+
Icon=${APP_NAME.toLowerCase()}
|
|
122
|
+
Terminal=false
|
|
123
|
+
Type=Application
|
|
124
|
+
Categories=Development;IDE;
|
|
125
|
+
StartupNotify=true
|
|
126
|
+
StartupWMClass=${APP_NAME}
|
|
127
|
+
`;
|
|
128
|
+
|
|
129
|
+
const desktopFile = path.join(applicationsDir, `${APP_NAME.toLowerCase()}.desktop`);
|
|
130
|
+
fs.writeFileSync(desktopFile, desktopContent);
|
|
131
|
+
|
|
132
|
+
await installLinuxIcon(artifactPath, options);
|
|
133
|
+
|
|
134
|
+
return { appPath: appImagePath, desktopFile };
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
async function installLinuxIcon(artifactPath, options) {
|
|
138
|
+
const iconsDir = path.join(os.homedir(), '.local', 'share', 'icons', 'hicolor');
|
|
139
|
+
const iconSizes = ['16x16', '32x32', '48x48', '64x64', '128x128', '256x256', '512x512'];
|
|
140
|
+
|
|
141
|
+
for (const size of iconSizes) {
|
|
142
|
+
const sizeDir = path.join(iconsDir, size, 'apps');
|
|
143
|
+
fs.mkdirSync(sizeDir, { recursive: true });
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const buildDir = path.join(path.dirname(artifactPath), '..', 'build');
|
|
147
|
+
if (fs.existsSync(buildDir)) {
|
|
148
|
+
for (const size of iconSizes) {
|
|
149
|
+
const srcIcon = path.join(buildDir, `${size}.png`);
|
|
150
|
+
if (fs.existsSync(srcIcon)) {
|
|
151
|
+
const destIcon = path.join(iconsDir, size, 'apps', `${APP_NAME.toLowerCase()}.png`);
|
|
152
|
+
fs.cpSync(srcIcon, destIcon);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
async function uninstallLinuxIntegration(options) {
|
|
159
|
+
const installDir = getInstallDir();
|
|
160
|
+
const applicationsDir = path.join(os.homedir(), '.local', 'share', 'applications');
|
|
161
|
+
|
|
162
|
+
const desktopFile = path.join(applicationsDir, `${APP_NAME.toLowerCase()}.desktop`);
|
|
163
|
+
if (fs.existsSync(desktopFile)) {
|
|
164
|
+
fs.unlinkSync(desktopFile);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
if (fs.existsSync(installDir)) {
|
|
168
|
+
fs.rmSync(installDir, { recursive: true, force: true });
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const iconsDir = path.join(os.homedir(), '.local', 'share', 'icons', 'hicolor');
|
|
172
|
+
const iconSizes = ['16x16', '32x32', '48x48', '64x64', '128x128', '256x256', '512x512'];
|
|
173
|
+
for (const size of iconSizes) {
|
|
174
|
+
const iconFile = path.join(iconsDir, size, 'apps', `${APP_NAME.toLowerCase()}.png`);
|
|
175
|
+
if (fs.existsSync(iconFile)) {
|
|
176
|
+
fs.unlinkSync(iconFile);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
async function installMacOSIntegration(artifactPath, version, options) {
|
|
182
|
+
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'archcoder-dmg-'));
|
|
183
|
+
const dmgPath = path.join(tempDir, `${APP_NAME}.dmg`);
|
|
184
|
+
const mountPoint = path.join(tempDir, 'mount');
|
|
185
|
+
|
|
186
|
+
fs.cpSync(artifactPath, dmgPath);
|
|
187
|
+
|
|
188
|
+
fs.mkdirSync(mountPoint, { recursive: true });
|
|
189
|
+
|
|
190
|
+
try {
|
|
191
|
+
await runCommand('hdiutil', ['attach', dmgPath, '-mountpoint', mountPoint, '-nobrowse']);
|
|
192
|
+
|
|
193
|
+
const sourceApp = path.join(mountPoint, `${APP_NAME}.app`);
|
|
194
|
+
const destApp = path.join('/Applications', `${APP_NAME}.app`);
|
|
195
|
+
|
|
196
|
+
if (fs.existsSync(destApp)) {
|
|
197
|
+
fs.rmSync(destApp, { recursive: true, force: true });
|
|
198
|
+
}
|
|
199
|
+
fs.cpSync(sourceApp, destApp);
|
|
200
|
+
|
|
201
|
+
await runCommand('hdiutil', ['detach', mountPoint]);
|
|
202
|
+
} finally {
|
|
203
|
+
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
return { appPath: `/Applications/${APP_NAME}.app` };
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
async function uninstallMacOSIntegration(options) {
|
|
210
|
+
const appPath = path.join('/Applications', `${APP_NAME}.app`);
|
|
211
|
+
|
|
212
|
+
if (fs.existsSync(appPath)) {
|
|
213
|
+
fs.rmSync(appPath, { recursive: true, force: true });
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
async function installWindowsIntegration(artifactPath, version, options) {
|
|
218
|
+
const installDir = getInstallDir();
|
|
219
|
+
fs.mkdirSync(installDir, { recursive: true });
|
|
220
|
+
|
|
221
|
+
const exePath = path.join(installDir, `${APP_NAME}.exe`);
|
|
222
|
+
|
|
223
|
+
if (fs.existsSync(exePath)) {
|
|
224
|
+
fs.unlinkSync(exePath);
|
|
225
|
+
}
|
|
226
|
+
fs.cpSync(artifactPath, exePath);
|
|
227
|
+
|
|
228
|
+
const appData = process.env.APPDATA || path.join(os.homedir(), 'AppData', 'Roaming');
|
|
229
|
+
const startMenuDir = path.join(appData, 'Microsoft', 'Windows', 'Start Menu', 'Programs', APP_NAME);
|
|
230
|
+
fs.mkdirSync(startMenuDir, { recursive: true });
|
|
231
|
+
|
|
232
|
+
const shortcutPath = path.join(startMenuDir, `${APP_NAME}.lnk`);
|
|
233
|
+
|
|
234
|
+
try {
|
|
235
|
+
const { shell } = await import('electron');
|
|
236
|
+
await shell.writeShortcutLink(shortcutPath, {
|
|
237
|
+
target: exePath,
|
|
238
|
+
cwd: installDir,
|
|
239
|
+
description: `${APP_NAME} - AI-powered coding assistant`,
|
|
240
|
+
});
|
|
241
|
+
} catch {
|
|
242
|
+
try {
|
|
243
|
+
const { default: createShortcut } = await import('windows-shortcuts');
|
|
244
|
+
await new Promise((resolve, reject) => {
|
|
245
|
+
createShortcut(shortcutPath, {
|
|
246
|
+
target: exePath,
|
|
247
|
+
cwd: installDir,
|
|
248
|
+
description: `${APP_NAME} - AI-powered coding assistant`,
|
|
249
|
+
}, err => {
|
|
250
|
+
if (err) reject(err);
|
|
251
|
+
else resolve();
|
|
252
|
+
});
|
|
253
|
+
});
|
|
254
|
+
} catch {
|
|
255
|
+
const batContent = `@echo off
|
|
256
|
+
start "" "${exePath}"
|
|
257
|
+
`;
|
|
258
|
+
const batPath = path.join(startMenuDir, `${APP_NAME}.bat`);
|
|
259
|
+
fs.writeFileSync(batPath, batContent);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
return { appPath: exePath, startMenuDir };
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
async function uninstallWindowsIntegration(options) {
|
|
267
|
+
const installDir = getInstallDir();
|
|
268
|
+
const appData = process.env.APPDATA || path.join(os.homedir(), 'AppData', 'Roaming');
|
|
269
|
+
const startMenuDir = path.join(appData, 'Microsoft', 'Windows', 'Start Menu', 'Programs', APP_NAME);
|
|
270
|
+
|
|
271
|
+
if (fs.existsSync(startMenuDir)) {
|
|
272
|
+
fs.rmSync(startMenuDir, { recursive: true, force: true });
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
if (fs.existsSync(installDir)) {
|
|
276
|
+
fs.rmSync(installDir, { recursive: true, force: true });
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
async function checkLinuxDependencies(options) {
|
|
281
|
+
return [];
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
export {
|
|
285
|
+
installDesktopIntegration,
|
|
286
|
+
uninstallDesktopIntegration,
|
|
287
|
+
getDesktopStatus,
|
|
288
|
+
checkLinuxDependencies,
|
|
289
|
+
};
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import crypto from 'crypto';
|
|
4
|
+
import { spawn } from 'child_process';
|
|
5
|
+
import { S3Client, GetObjectCommand } from '@aws-sdk/client-s3';
|
|
6
|
+
import { getElectronArtifactName, getCacheDir } from './platform.js';
|
|
7
|
+
|
|
8
|
+
const MAX_RETRIES = 3;
|
|
9
|
+
const RETRY_DELAYS = [1000, 2000, 4000];
|
|
10
|
+
|
|
11
|
+
const S3_CONFIG = {
|
|
12
|
+
endpoint: process.env.ARCHCODER_S3_ENDPOINT || 'https://minio-service.archlast.com',
|
|
13
|
+
bucket: process.env.ARCHCODER_S3_BUCKET || 'archcoder-releases',
|
|
14
|
+
region: process.env.ARCHCODER_S3_REGION || 'auto',
|
|
15
|
+
accessKeyId: process.env.ARCHCODER_S3_ACCESS_KEY || 'z7nBQ7myyoTgRi3C',
|
|
16
|
+
secretAccessKey: process.env.ARCHCODER_S3_SECRET_KEY || 'Yjcbc2yVGmycJWeWZrqOEN5ZSaFPYo68',
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
function getS3Client() {
|
|
20
|
+
return new S3Client({
|
|
21
|
+
endpoint: S3_CONFIG.endpoint,
|
|
22
|
+
region: S3_CONFIG.region,
|
|
23
|
+
credentials: S3_CONFIG.accessKeyId && S3_CONFIG.secretAccessKey ? {
|
|
24
|
+
accessKeyId: S3_CONFIG.accessKeyId,
|
|
25
|
+
secretAccessKey: S3_CONFIG.secretAccessKey,
|
|
26
|
+
} : undefined,
|
|
27
|
+
forcePathStyle: true,
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async function sleep(ms) {
|
|
32
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function computeSHA256(filePath) {
|
|
36
|
+
return new Promise((resolve, reject) => {
|
|
37
|
+
const hash = crypto.createHash('sha256');
|
|
38
|
+
const stream = fs.createReadStream(filePath);
|
|
39
|
+
|
|
40
|
+
stream.on('data', chunk => hash.update(chunk));
|
|
41
|
+
stream.on('end', () => resolve(hash.digest('hex')));
|
|
42
|
+
stream.on('error', reject);
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async function getLatestVersion() {
|
|
47
|
+
const client = getS3Client();
|
|
48
|
+
|
|
49
|
+
try {
|
|
50
|
+
const command = new GetObjectCommand({
|
|
51
|
+
Bucket: S3_CONFIG.bucket,
|
|
52
|
+
Key: 'releases/latest.json',
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
const response = await client.send(command);
|
|
56
|
+
const body = await streamToString(response.Body);
|
|
57
|
+
const data = JSON.parse(body);
|
|
58
|
+
|
|
59
|
+
return data.version;
|
|
60
|
+
} catch (error) {
|
|
61
|
+
if (error.name === 'NoSuchKey' || error.$metadata?.httpStatusCode === 404) {
|
|
62
|
+
throw new Error('No releases found. Please check if binaries have been published.');
|
|
63
|
+
}
|
|
64
|
+
throw error;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async function streamToString(stream) {
|
|
69
|
+
const chunks = [];
|
|
70
|
+
for await (const chunk of stream) {
|
|
71
|
+
chunks.push(chunk);
|
|
72
|
+
}
|
|
73
|
+
return Buffer.concat(chunks).toString('utf-8');
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
async function downloadFromS3(key, destPath, onProgress, options = {}) {
|
|
77
|
+
const client = getS3Client();
|
|
78
|
+
|
|
79
|
+
const command = new GetObjectCommand({
|
|
80
|
+
Bucket: S3_CONFIG.bucket,
|
|
81
|
+
Key: key,
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
let lastError;
|
|
85
|
+
|
|
86
|
+
for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
|
|
87
|
+
try {
|
|
88
|
+
const response = await client.send(command);
|
|
89
|
+
|
|
90
|
+
if (!response.Body) {
|
|
91
|
+
throw new Error('No response body from S3');
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const contentLength = response.ContentLength || 0;
|
|
95
|
+
let downloaded = 0;
|
|
96
|
+
|
|
97
|
+
fs.mkdirSync(path.dirname(destPath), { recursive: true });
|
|
98
|
+
|
|
99
|
+
const fileStream = fs.createWriteStream(destPath);
|
|
100
|
+
|
|
101
|
+
for await (const chunk of response.Body) {
|
|
102
|
+
fileStream.write(chunk);
|
|
103
|
+
downloaded += chunk.length;
|
|
104
|
+
|
|
105
|
+
if (onProgress && contentLength > 0) {
|
|
106
|
+
const progress = Math.round((downloaded / contentLength) * 100);
|
|
107
|
+
onProgress(progress);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
fileStream.end();
|
|
112
|
+
|
|
113
|
+
await new Promise((resolve, reject) => {
|
|
114
|
+
fileStream.on('finish', resolve);
|
|
115
|
+
fileStream.on('error', reject);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
return;
|
|
119
|
+
} catch (error) {
|
|
120
|
+
lastError = error;
|
|
121
|
+
|
|
122
|
+
if (error.name === 'NoSuchKey' || error.$metadata?.httpStatusCode === 404) {
|
|
123
|
+
throw new Error(`Desktop binary not found: ${key}\n` +
|
|
124
|
+
`S3 bucket: ${S3_CONFIG.bucket}\n` +
|
|
125
|
+
`Hint: Make sure Electron binaries have been built and uploaded to S3`);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (attempt < MAX_RETRIES) {
|
|
129
|
+
await sleep(RETRY_DELAYS[attempt]);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
throw lastError;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
async function getBinaryChecksum(version, archiveName) {
|
|
138
|
+
try {
|
|
139
|
+
const client = getS3Client();
|
|
140
|
+
|
|
141
|
+
const command = new GetObjectCommand({
|
|
142
|
+
Bucket: S3_CONFIG.bucket,
|
|
143
|
+
Key: `releases/v${version}/${archiveName}.sha256`,
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
const response = await client.send(command);
|
|
147
|
+
const body = await streamToString(response.Body);
|
|
148
|
+
|
|
149
|
+
return body.trim().split(/\s+/)[0];
|
|
150
|
+
} catch {
|
|
151
|
+
return null;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
async function downloadBinary({ version, target, quiet, onProgress, options }) {
|
|
156
|
+
const artifactName = getElectronArtifactName(version, target);
|
|
157
|
+
|
|
158
|
+
let actualVersion = version;
|
|
159
|
+
if (version === 'latest' || !version) {
|
|
160
|
+
actualVersion = await getLatestVersion();
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const s3Key = `releases/v${actualVersion}/${artifactName}`;
|
|
164
|
+
|
|
165
|
+
const cacheDir = getCacheDir();
|
|
166
|
+
const destPath = path.join(cacheDir, actualVersion, artifactName);
|
|
167
|
+
|
|
168
|
+
if (fs.existsSync(destPath)) {
|
|
169
|
+
if (!quiet && shouldRenderHumanOutput(options)) {
|
|
170
|
+
console.log(`Using cached artifact: ${destPath}`);
|
|
171
|
+
}
|
|
172
|
+
return destPath;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
fs.mkdirSync(path.dirname(destPath), { recursive: true });
|
|
176
|
+
|
|
177
|
+
await downloadFromS3(s3Key, destPath, onProgress, options);
|
|
178
|
+
|
|
179
|
+
const expectedChecksum = await getBinaryChecksum(actualVersion, artifactName);
|
|
180
|
+
|
|
181
|
+
if (expectedChecksum) {
|
|
182
|
+
const actualChecksum = await computeSHA256(destPath);
|
|
183
|
+
|
|
184
|
+
if (expectedChecksum !== actualChecksum) {
|
|
185
|
+
fs.unlinkSync(destPath);
|
|
186
|
+
throw new Error(`Checksum verification failed. Expected ${expectedChecksum}, got ${actualChecksum}`);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const isLinux = target.includes('linux');
|
|
191
|
+
if (isLinux) {
|
|
192
|
+
fs.chmodSync(destPath, 0o755);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
return destPath;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function shouldRenderHumanOutput(options) {
|
|
199
|
+
return !options?.json && !options?.quiet;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
export {
|
|
203
|
+
downloadBinary,
|
|
204
|
+
computeSHA256,
|
|
205
|
+
getLatestVersion,
|
|
206
|
+
getS3Client,
|
|
207
|
+
S3_CONFIG,
|
|
208
|
+
};
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import {
|
|
2
|
+
getDataDir,
|
|
3
|
+
getPlatformTarget,
|
|
4
|
+
getElectronArtifactName,
|
|
5
|
+
getBinaryName,
|
|
6
|
+
getBinaryPath,
|
|
7
|
+
getCacheDir,
|
|
8
|
+
getVersionFilePath,
|
|
9
|
+
getInstallDir,
|
|
10
|
+
} from './platform.js';
|
|
11
|
+
|
|
12
|
+
import {
|
|
13
|
+
downloadBinary,
|
|
14
|
+
computeSHA256,
|
|
15
|
+
getLatestVersion,
|
|
16
|
+
getS3Client,
|
|
17
|
+
S3_CONFIG,
|
|
18
|
+
} from './download.js';
|
|
19
|
+
|
|
20
|
+
import {
|
|
21
|
+
installDesktopIntegration,
|
|
22
|
+
uninstallDesktopIntegration,
|
|
23
|
+
getDesktopStatus,
|
|
24
|
+
checkLinuxDependencies,
|
|
25
|
+
} from './desktop.js';
|
|
26
|
+
|
|
27
|
+
export {
|
|
28
|
+
getDataDir,
|
|
29
|
+
getPlatformTarget,
|
|
30
|
+
getElectronArtifactName,
|
|
31
|
+
getBinaryName,
|
|
32
|
+
getBinaryPath,
|
|
33
|
+
getCacheDir,
|
|
34
|
+
getVersionFilePath,
|
|
35
|
+
getInstallDir,
|
|
36
|
+
downloadBinary,
|
|
37
|
+
computeSHA256,
|
|
38
|
+
getLatestVersion,
|
|
39
|
+
getS3Client,
|
|
40
|
+
S3_CONFIG,
|
|
41
|
+
installDesktopIntegration,
|
|
42
|
+
uninstallDesktopIntegration,
|
|
43
|
+
getDesktopStatus,
|
|
44
|
+
checkLinuxDependencies,
|
|
45
|
+
};
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import os from 'os';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
|
|
4
|
+
function getDataDir() {
|
|
5
|
+
return process.env.ARCHCODER_DATA_DIR || path.join(os.homedir(), '.config', 'archcoder');
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
function getPlatformTarget() {
|
|
9
|
+
const platform = process.platform;
|
|
10
|
+
const arch = process.arch;
|
|
11
|
+
|
|
12
|
+
const platformMap = {
|
|
13
|
+
darwin: {
|
|
14
|
+
arm64: 'arm64-darwin',
|
|
15
|
+
x64: 'x64-darwin',
|
|
16
|
+
},
|
|
17
|
+
linux: {
|
|
18
|
+
arm64: 'arm64-linux',
|
|
19
|
+
x64: 'x64-linux',
|
|
20
|
+
},
|
|
21
|
+
win32: {
|
|
22
|
+
x64: 'x64-windows',
|
|
23
|
+
arm64: 'arm64-windows',
|
|
24
|
+
},
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const target = platformMap[platform]?.[arch];
|
|
28
|
+
|
|
29
|
+
if (!target) {
|
|
30
|
+
throw new Error(`Unsupported platform: ${platform} ${arch}`);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return target;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function getElectronArtifactName(version, target) {
|
|
37
|
+
const isWindows = target.includes('windows');
|
|
38
|
+
const isMac = target.includes('darwin');
|
|
39
|
+
const isLinux = target.includes('linux');
|
|
40
|
+
const isArm = target.includes('arm64');
|
|
41
|
+
|
|
42
|
+
if (isWindows) {
|
|
43
|
+
return `ArchCoder-${version}-x64.exe`;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (isMac) {
|
|
47
|
+
const arch = isArm ? 'arm64' : 'x64';
|
|
48
|
+
return `ArchCoder-${version}-${arch}.dmg`;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (isLinux) {
|
|
52
|
+
const arch = isArm ? 'arm64' : 'x86_64';
|
|
53
|
+
return `ArchCoder-${version}-${arch}.AppImage`;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
throw new Error(`Unknown target: ${target}`);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function getBinaryName(version, target) {
|
|
60
|
+
const isWindows = target.includes('windows');
|
|
61
|
+
const ext = isWindows ? '.exe' : '';
|
|
62
|
+
return `archcoder-server-${target}${ext}`;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function getBinaryPath(version) {
|
|
66
|
+
const target = getPlatformTarget();
|
|
67
|
+
const binaryName = getBinaryName(version, target);
|
|
68
|
+
return path.join(getDataDir(), 'binaries', binaryName);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function getCacheDir() {
|
|
72
|
+
return path.join(getDataDir(), 'cache', 'binaries');
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function getVersionFilePath() {
|
|
76
|
+
return path.join(getDataDir(), 'binaries', '.version');
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function getInstallDir() {
|
|
80
|
+
const platform = process.platform;
|
|
81
|
+
|
|
82
|
+
if (platform === 'darwin') {
|
|
83
|
+
return path.join(os.homedir(), '.local', 'share', 'archcoder');
|
|
84
|
+
} else if (platform === 'win32') {
|
|
85
|
+
return path.join(process.env.LOCALAPPDATA || path.join(os.homedir(), 'AppData', 'Local'), 'ArchCoder');
|
|
86
|
+
} else {
|
|
87
|
+
return path.join(os.homedir(), '.local', 'share', 'archcoder');
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export {
|
|
92
|
+
getDataDir,
|
|
93
|
+
getPlatformTarget,
|
|
94
|
+
getElectronArtifactName,
|
|
95
|
+
getBinaryName,
|
|
96
|
+
getBinaryPath,
|
|
97
|
+
getCacheDir,
|
|
98
|
+
getVersionFilePath,
|
|
99
|
+
getInstallDir,
|
|
100
|
+
};
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# Notifications Module Documentation
|
|
2
|
+
|
|
3
|
+
## Purpose
|
|
4
|
+
This module provides notification message preparation utilities for the web server runtime, including text truncation and optional message summarization for system notifications.
|
|
5
|
+
|
|
6
|
+
## Entrypoints and structure
|
|
7
|
+
- `packages/web/server/lib/notifications/index.js`: public entrypoint imported by `packages/web/server/index.js`.
|
|
8
|
+
- `packages/web/server/lib/notifications/message.js`: helper implementation module.
|
|
9
|
+
- `packages/web/server/lib/notifications/message.test.js`: unit tests for notification message helpers.
|
|
10
|
+
|
|
11
|
+
## Public exports
|
|
12
|
+
|
|
13
|
+
### Notifications API (re-exported from message.js)
|
|
14
|
+
- `truncateNotificationText(text, maxLength)`: Truncates text to specified max length, appending `...` if truncated.
|
|
15
|
+
- `prepareNotificationLastMessage({ message, settings, summarize })`: Prepares the last message for notification display, with optional summarization support.
|
|
16
|
+
|
|
17
|
+
## Constants
|
|
18
|
+
|
|
19
|
+
### Default values
|
|
20
|
+
- `DEFAULT_NOTIFICATION_MESSAGE_MAX_LENGTH`: 250 (default max length for notification text).
|
|
21
|
+
- `DEFAULT_NOTIFICATION_SUMMARY_THRESHOLD`: 200 (minimum message length to trigger summarization).
|
|
22
|
+
- `DEFAULT_NOTIFICATION_SUMMARY_LENGTH`: 100 (target length for summarized messages).
|
|
23
|
+
|
|
24
|
+
## Settings object format
|
|
25
|
+
|
|
26
|
+
The `settings` parameter for `prepareNotificationLastMessage` supports:
|
|
27
|
+
- `summarizeLastMessage` (boolean): Whether to enable summarization for long messages.
|
|
28
|
+
- `summaryThreshold` (number): Minimum message length to trigger summarization (default: 200).
|
|
29
|
+
- `summaryLength` (number): Target length for summarized messages (default: 100).
|
|
30
|
+
- `maxLastMessageLength` (number): Maximum length for the final notification text (default: 250).
|
|
31
|
+
|
|
32
|
+
## Response contracts
|
|
33
|
+
|
|
34
|
+
### `truncateNotificationText`
|
|
35
|
+
- Returns empty string for non-string input.
|
|
36
|
+
- Returns original text if under max length.
|
|
37
|
+
- Returns `${text.slice(0, maxLength)}...` for truncated text.
|
|
38
|
+
|
|
39
|
+
### `prepareNotificationLastMessage`
|
|
40
|
+
- Returns empty string for empty/null message.
|
|
41
|
+
- Returns truncated original message if summarization disabled, message under threshold, or summarization fails.
|
|
42
|
+
- Returns truncated summary if summarization succeeds and returns non-empty string.
|
|
43
|
+
- Always applies `maxLastMessageLength` truncation to final result.
|
|
44
|
+
|
|
45
|
+
## Notes for contributors
|
|
46
|
+
|
|
47
|
+
### Adding new notification helpers
|
|
48
|
+
1. Add new helper functions to `packages/web/server/lib/notifications/message.js`.
|
|
49
|
+
2. Export functions that are intended for public use.
|
|
50
|
+
3. Follow existing patterns for input validation (e.g., type checking for strings).
|
|
51
|
+
4. Use `resolvePositiveNumber` for numeric parameters with fallbacks to maintain safe defaults.
|
|
52
|
+
5. Add corresponding unit tests in `packages/web/server/lib/notifications/message.test.js`.
|
|
53
|
+
|
|
54
|
+
### Error handling
|
|
55
|
+
- `prepareNotificationLastMessage` catches summarization errors and falls back to original message.
|
|
56
|
+
- Invalid numeric parameters default to safe fallback values.
|
|
57
|
+
- Non-string inputs are handled gracefully (return empty string).
|
|
58
|
+
|
|
59
|
+
### Testing
|
|
60
|
+
- Run `bun run type-check`, `bun run lint`, and `bun run build` before finalizing changes.
|
|
61
|
+
- Unit tests should cover truncation behavior, summarization success/failure, and edge cases (empty strings, invalid inputs).
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { truncateNotificationText, prepareNotificationLastMessage } from './message.js';
|