Skip to content

Commit b073c41

Browse files
authored
Merge pull request #3 from Shikakiben/test
install AM si non detecté
2 parents 5186288 + 0089242 commit b073c41

File tree

11 files changed

+845
-17
lines changed

11 files changed

+845
-17
lines changed

.github/workflows/ci.yml

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
name: CI
2+
3+
on:
4+
push:
5+
branches: ["main", "test"]
6+
pull_request:
7+
branches: ["main", "test"]
8+
9+
jobs:
10+
build:
11+
runs-on: ubuntu-latest
12+
steps:
13+
- name: Checkout repository
14+
uses: actions/checkout@v4
15+
16+
- name: Setup Node.js
17+
uses: actions/setup-node@v4
18+
with:
19+
node-version: '20'
20+
cache: npm
21+
22+
- name: Install dependencies
23+
run: npm ci
24+
25+
- name: Lint
26+
run: npm run lint
27+
28+
- name: Verify main-process modules
29+
run: node scripts/verify-main-modules.js
30+
31+
- name: Verify renderer queue logic
32+
run: node scripts/verify-renderer-queue.js

index.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -133,7 +133,7 @@ <h2 class="panel-heading-sm centered" data-i18n="advanced.title">Mode avancé</h
133133
<li><code>am translate</code><span data-i18n="advanced.cmd.translate">gérer traductions</span></li>
134134
</ul>
135135

136-
<p class="note"><em><span data-i18n="advanced.note.appman">Pour la version sans privilèges, remplacez <code>am</code> par <code>appman</code>.</span></em></p>
136+
<p class="note"><em><span data-i18n-html="advanced.note.appman">Pour la version sans privilèges, remplacez <code>am</code> par <code>appman</code>.</span></em></p>
137137

138138
<p class="docs-link"><a href="https://github.com/ivan-hc/AM" target="_blank" rel="noopener"><span data-i18n="advanced.docs">Documentation complète & guides →</span> AM (GitHub)</a></p>
139139
</section>

main.js

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ const { exec, spawn } = require('child_process');
55
const { registerCategoryHandlers } = require('./src/main/categories');
66
const { detectPackageManager, invalidatePackageManagerCache } = require('./src/main/packageManager');
77
const { createIconCacheManager } = require('./src/main/iconCache');
8+
const { installAppManAuto } = require('./src/main/appManAuto');
89

910
const iconCacheManager = createIconCacheManager(app);
1011
registerCategoryHandlers(ipcMain);
@@ -443,6 +444,16 @@ ipcMain.handle('install-send-choice', async (_event, installId, choice) => {
443444
}
444445
});
445446

447+
ipcMain.handle('install-appman-auto', async () => {
448+
try {
449+
const result = await installAppManAuto();
450+
invalidatePackageManagerCache();
451+
return { ok: true, result };
452+
} catch (error) {
453+
return { ok: false, error: error?.message || 'Installation AppMan échouée.' };
454+
}
455+
});
456+
446457

447458
// Liste détaillée: distingue installées vs catalogue
448459
ipcMain.handle('list-apps-detailed', async () => {

preload.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ contextBridge.exposeInMainWorld('electronAPI', {
1919
installCancel: (id) => ipcRenderer.invoke('install-cancel', id, id),
2020
installSendChoice: (id, choice) => ipcRenderer.invoke('install-send-choice', id, choice),
2121
onInstallProgress: (cb) => ipcRenderer.on('install-progress', (e, msg) => cb && cb(msg)),
22+
installAppManAuto: () => ipcRenderer.invoke('install-appman-auto'),
2223
purgeIconsCache: () => ipcRenderer.invoke('purge-icons-cache'),
2324
getGpuPref: () => ipcRenderer.invoke('get-gpu-pref'),
2425
setGpuPref: (val) => ipcRenderer.invoke('set-gpu-pref', val),

scripts/verify-main-modules.js

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,13 @@ const fs = require('fs');
22
const os = require('os');
33
const path = require('path');
44
const childProcess = require('child_process');
5+
const undici = require('undici');
56

67
const originalExec = childProcess.exec;
8+
const originalFetch = undici.fetch;
79
let tempDir = null;
810
let success = false;
11+
let categoriesBackup = null;
912

1013
(async () => {
1114
let execInvocations = 0;
@@ -30,7 +33,54 @@ let success = false;
3033
});
3134
};
3235

36+
const repoRoot = path.resolve(__dirname, '..');
37+
const categoriesCachePath = path.join(repoRoot, 'categories-cache.json');
38+
const categoriesMetaPath = path.join(repoRoot, 'categories-cache.meta.json');
39+
40+
function snapshotCategoriesFiles() {
41+
return {
42+
cache: fs.existsSync(categoriesCachePath) ? fs.readFileSync(categoriesCachePath) : null,
43+
meta: fs.existsSync(categoriesMetaPath) ? fs.readFileSync(categoriesMetaPath) : null
44+
};
45+
}
46+
47+
function restoreCategoriesFiles(snapshot) {
48+
if (!snapshot) return;
49+
if (snapshot.cache) fs.writeFileSync(categoriesCachePath, snapshot.cache);
50+
else if (fs.existsSync(categoriesCachePath)) fs.rmSync(categoriesCachePath);
51+
if (snapshot.meta) fs.writeFileSync(categoriesMetaPath, snapshot.meta);
52+
else if (fs.existsSync(categoriesMetaPath)) fs.rmSync(categoriesMetaPath);
53+
}
54+
55+
function createHeadersProxy(headers = {}) {
56+
const normalized = Object.fromEntries(
57+
Object.entries(headers).map(([k, v]) => [String(k).toLowerCase(), v])
58+
);
59+
return {
60+
get(name) {
61+
return normalized[String(name).toLowerCase()] || null;
62+
}
63+
};
64+
}
65+
66+
function createResponse({ status = 200, ok = true, jsonData = null, textData = '', headers = {} }) {
67+
return {
68+
status,
69+
ok,
70+
async json() {
71+
if (jsonData === null) throw new Error('JSON payload missing');
72+
return jsonData;
73+
},
74+
async text() {
75+
return textData;
76+
},
77+
headers: createHeadersProxy(headers)
78+
};
79+
}
80+
3381
try {
82+
categoriesBackup = snapshotCategoriesFiles();
83+
3484
const { detectPackageManager, invalidatePackageManagerCache } = require('../src/main/packageManager');
3585
const { createIconCacheManager } = require('../src/main/iconCache');
3686

@@ -82,6 +132,71 @@ let success = false;
82132
if (!purgeResult || purgeResult.removed < 1) throw new Error('Purge cache did not report removed files');
83133
if (fs.existsSync(fakeIconPath)) throw new Error('Purge cache did not delete dummy icon');
84134

135+
const fileEtags = new Map();
136+
const markdownByFile = {
137+
'games.md': `| App | Desc |\n| --- | --- |\n| ***alpha*** | great app |`,
138+
'tools.md': `| App | Desc |\n| --- | --- |\n| ***beta*** | tool desc |`
139+
};
140+
141+
undici.fetch = async (url, options = {}) => {
142+
const headers = options.headers || {};
143+
if (url.endsWith('/contents')) {
144+
return createResponse({
145+
jsonData: [
146+
{ name: 'games.md' },
147+
{ name: 'tools.md' },
148+
{ name: 'README.md' }
149+
]
150+
});
151+
}
152+
const fileName = path.basename(url);
153+
if (!markdownByFile[fileName]) {
154+
return createResponse({ status: 404, ok: false, textData: 'missing' });
155+
}
156+
const previousEtag = fileEtags.get(fileName);
157+
if (headers['If-None-Match'] && previousEtag && headers['If-None-Match'] === previousEtag) {
158+
return createResponse({ status: 304, ok: false });
159+
}
160+
const nextEtag = `W/"etag-${fileName}-${Date.now()}"`;
161+
fileEtags.set(fileName, nextEtag);
162+
return createResponse({
163+
textData: markdownByFile[fileName],
164+
headers: { etag: nextEtag, 'last-modified': new Date().toUTCString() }
165+
});
166+
};
167+
168+
const { registerCategoryHandlers } = require('../src/main/categories');
169+
const ipcHandlers = new Map();
170+
const ipcMain = {
171+
handle(channel, handler) {
172+
ipcHandlers.set(channel, handler);
173+
}
174+
};
175+
176+
registerCategoryHandlers(ipcMain);
177+
if (!ipcHandlers.has('fetch-all-categories')) throw new Error('fetch-all-categories handler missing');
178+
179+
const firstFetch = await ipcHandlers.get('fetch-all-categories')();
180+
if (!firstFetch.ok) throw new Error(`fetch-all-categories failed: ${firstFetch.error}`);
181+
if (!Array.isArray(firstFetch.categories) || firstFetch.categories.length !== 2) {
182+
throw new Error('Unexpected categories payload on first fetch');
183+
}
184+
if (!firstFetch.categories[0].apps.includes('alpha')) throw new Error('Category parsing failed');
185+
186+
const cached = await ipcHandlers.get('get-categories-cache')();
187+
if (!cached.ok || cached.categories.length !== 2) throw new Error('get-categories-cache returned invalid data');
188+
189+
const secondFetch = await ipcHandlers.get('fetch-all-categories')();
190+
if (!secondFetch.ok) throw new Error('Second fetch should still be ok (even with 304)');
191+
if (secondFetch.categories.length !== firstFetch.categories.length) {
192+
throw new Error('Second fetch should reuse cached categories when not modified');
193+
}
194+
195+
const deleteResult = await ipcHandlers.get('delete-categories-cache')();
196+
if (!deleteResult.ok) throw new Error('delete-categories-cache failed');
197+
if (fs.existsSync(categoriesCachePath)) throw new Error('Cache file still present after deletion');
198+
if (fs.existsSync(categoriesMetaPath)) throw new Error('Cache meta file still present after deletion');
199+
85200
success = true;
86201
console.log('✔ Main-process modules verified successfully.');
87202
} catch (err) {
@@ -96,6 +211,8 @@ let success = false;
96211
console.warn('Warning: failed to clean temporary directory:', cleanupErr.message);
97212
}
98213
childProcess.exec = originalExec;
214+
undici.fetch = originalFetch;
215+
restoreCategoriesFiles(categoriesBackup);
99216
if (success) {
100217
process.exitCode = 0;
101218
}

scripts/verify-renderer-queue.js

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
const fs = require('fs');
2+
const path = require('path');
3+
const vm = require('vm');
4+
5+
function extractQueueSnippet(fullSource) {
6+
const startMarker = 'let activeInstallSession = {';
7+
const endMarker = 'let syncBtn = null;';
8+
const start = fullSource.indexOf(startMarker);
9+
const end = fullSource.indexOf(endMarker);
10+
if (start === -1 || end === -1 || end <= start) {
11+
throw new Error('Unable to locate install queue section in renderer.js');
12+
}
13+
return fullSource.slice(start, end);
14+
}
15+
16+
function createDocumentStub() {
17+
const emptyArray = [];
18+
return {
19+
body: { classList: { toggle() {}, add() {}, remove() {} } },
20+
querySelectorAll() { return emptyArray; },
21+
querySelector() { return null; }
22+
};
23+
}
24+
25+
function createButtonStub(name) {
26+
const attrs = { 'data-name': name };
27+
return {
28+
hidden: false,
29+
disabled: false,
30+
textContent: '',
31+
classList: { add() {}, remove() {} },
32+
setAttribute(key, value) { attrs[key] = value; },
33+
getAttribute(key) { return attrs[key]; }
34+
};
35+
}
36+
37+
function assert(condition, message) {
38+
if (!condition) throw new Error(message);
39+
}
40+
41+
(async () => {
42+
const rendererPath = path.join(__dirname, '..', 'src', 'renderer', 'renderer.js');
43+
const rendererSource = fs.readFileSync(rendererPath, 'utf8');
44+
const queueSnippet = extractQueueSnippet(rendererSource);
45+
const instrumentedSnippet = `${queueSnippet}\n globalThis.__queueTest = { installQueue, enqueueInstall, removeFromQueue, processNextInstall, activeInstallSession };`;
46+
47+
const toastMessages = [];
48+
const queueIndicatorsUpdates = [];
49+
const startStreamingCalls = [];
50+
51+
const context = {
52+
console,
53+
setTimeout,
54+
clearTimeout,
55+
CSS: { escape: (value) => value },
56+
Node: { TEXT_NODE: 3 },
57+
window: {
58+
electronAPI: {
59+
amAction: () => Promise.resolve()
60+
}
61+
},
62+
document: createDocumentStub(),
63+
detailsInstallBtn: createButtonStub('alpha'),
64+
state: { viewMode: 'grid', installed: new Set(), filtered: [] },
65+
showToast(msg) { toastMessages.push(msg); },
66+
t(key, vars) {
67+
if (!vars) return key;
68+
return `${key}:${JSON.stringify(vars)}`;
69+
},
70+
startStreamingInstall(name) {
71+
startStreamingCalls.push(name);
72+
return Promise.resolve();
73+
},
74+
loadApps: () => Promise.resolve(),
75+
applySearch: () => {},
76+
refreshAllInstallButtons: () => {},
77+
refreshTileBadges: () => {},
78+
updateQueueIndicators: () => queueIndicatorsUpdates.push(Date.now())
79+
};
80+
81+
vm.createContext(context);
82+
vm.runInContext(instrumentedSnippet, context);
83+
84+
const { installQueue, enqueueInstall, removeFromQueue, processNextInstall, activeInstallSession } = context.__queueTest || {};
85+
86+
assert(Array.isArray(installQueue), 'installQueue array not exposed');
87+
88+
activeInstallSession.id = null;
89+
activeInstallSession.done = true;
90+
enqueueInstall('alpha');
91+
assert(startStreamingCalls.includes('alpha'), 'Immediate install should start streaming');
92+
assert(installQueue.length === 0, 'Queue should be empty after starting install');
93+
94+
activeInstallSession.id = 'session-1';
95+
activeInstallSession.name = 'alpha';
96+
activeInstallSession.done = false;
97+
enqueueInstall('beta');
98+
assert(installQueue.length === 1, 'Item should be queued while install active');
99+
100+
const removed = removeFromQueue('beta');
101+
assert(removed === true, 'removeFromQueue should return true when entry existed');
102+
assert(installQueue.length === 0, 'Queue should be empty after removal');
103+
104+
enqueueInstall('gamma');
105+
assert(installQueue.length === 1, 'Queue should contain gamma before processing');
106+
107+
activeInstallSession.id = null;
108+
activeInstallSession.done = true;
109+
processNextInstall();
110+
assert(startStreamingCalls.includes('gamma'), 'processNextInstall should start gamma streaming');
111+
assert(queueIndicatorsUpdates.length >= 1, 'Queue indicators should update when queue changes');
112+
113+
console.log('✔ Renderer queue logic verified successfully.');
114+
})().catch((err) => {
115+
console.error('✖ Renderer queue verification failed:', err);
116+
process.exitCode = 1;
117+
});

src/main/appManAuto.js

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
const fs = require('fs');
2+
const os = require('os');
3+
const path = require('path');
4+
const { fetch } = require('undici');
5+
6+
const APPMAN_URL = 'https://raw.githubusercontent.com/ivan-hc/AM/main/APP-MANAGER';
7+
const USER_AGENT = 'AM-GUI';
8+
9+
async function installAppManAuto() {
10+
const home = os.homedir();
11+
const bindir = process.env.XDG_BIN_HOME || path.join(home, '.local', 'bin');
12+
fs.mkdirSync(bindir, { recursive: true });
13+
const res = await fetch(APPMAN_URL, { headers: { 'User-Agent': USER_AGENT } });
14+
if (!res.ok) {
15+
throw new Error(`Téléchargement échoué (HTTP ${res.status})`);
16+
}
17+
const buffer = Buffer.from(await res.arrayBuffer());
18+
const target = path.join(bindir, 'appman');
19+
fs.writeFileSync(target, buffer, { mode: 0o755 });
20+
const configDir = path.join(home, '.config', 'appman');
21+
fs.mkdirSync(configDir, { recursive: true });
22+
const configPath = path.join(configDir, 'appman-config');
23+
if (!fs.existsSync(configPath)) {
24+
const defaultDir = path.join(home, 'Applications');
25+
fs.mkdirSync(defaultDir, { recursive: true });
26+
fs.writeFileSync(configPath, defaultDir);
27+
}
28+
return { target, bindir };
29+
}
30+
31+
module.exports = { installAppManAuto };

0 commit comments

Comments
 (0)