@y4wee/nupo 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/dist/App.d.ts +8 -0
- package/dist/App.js +109 -0
- package/dist/__tests__/checks.test.d.ts +1 -0
- package/dist/__tests__/checks.test.js +68 -0
- package/dist/__tests__/config.test.d.ts +1 -0
- package/dist/__tests__/config.test.js +61 -0
- package/dist/components/ConfirmExit.d.ts +9 -0
- package/dist/components/ConfirmExit.js +12 -0
- package/dist/components/ErrorPanel.d.ts +7 -0
- package/dist/components/ErrorPanel.js +12 -0
- package/dist/components/Header.d.ts +10 -0
- package/dist/components/Header.js +17 -0
- package/dist/components/LeftPanel.d.ts +9 -0
- package/dist/components/LeftPanel.js +31 -0
- package/dist/components/OptionsPanel.d.ts +11 -0
- package/dist/components/OptionsPanel.js +18 -0
- package/dist/components/PathInput.d.ts +10 -0
- package/dist/components/PathInput.js +133 -0
- package/dist/components/ProgressBar.d.ts +5 -0
- package/dist/components/ProgressBar.js +21 -0
- package/dist/components/StepsPanel.d.ts +8 -0
- package/dist/components/StepsPanel.js +24 -0
- package/dist/hooks/useConfig.d.ts +8 -0
- package/dist/hooks/useConfig.js +26 -0
- package/dist/hooks/useTerminalSize.d.ts +4 -0
- package/dist/hooks/useTerminalSize.js +18 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +169 -0
- package/dist/screens/ConfigScreen.d.ts +10 -0
- package/dist/screens/ConfigScreen.js +182 -0
- package/dist/screens/ConfigureServiceScreen.d.ts +11 -0
- package/dist/screens/ConfigureServiceScreen.js +499 -0
- package/dist/screens/HomeScreen.d.ts +14 -0
- package/dist/screens/HomeScreen.js +24 -0
- package/dist/screens/IdeScreen.d.ts +9 -0
- package/dist/screens/IdeScreen.js +101 -0
- package/dist/screens/InitScreen.d.ts +9 -0
- package/dist/screens/InitScreen.js +182 -0
- package/dist/screens/InstallVersionScreen.d.ts +10 -0
- package/dist/screens/InstallVersionScreen.js +495 -0
- package/dist/screens/OdooScreen.d.ts +13 -0
- package/dist/screens/OdooScreen.js +76 -0
- package/dist/screens/OdooServiceScreen.d.ts +10 -0
- package/dist/screens/OdooServiceScreen.js +51 -0
- package/dist/screens/StartServiceScreen.d.ts +12 -0
- package/dist/screens/StartServiceScreen.js +386 -0
- package/dist/screens/UpgradeVersionScreen.d.ts +9 -0
- package/dist/screens/UpgradeVersionScreen.js +259 -0
- package/dist/services/checks.d.ts +8 -0
- package/dist/services/checks.js +48 -0
- package/dist/services/config.d.ts +11 -0
- package/dist/services/config.js +146 -0
- package/dist/services/git.d.ts +35 -0
- package/dist/services/git.js +173 -0
- package/dist/services/ide.d.ts +10 -0
- package/dist/services/ide.js +126 -0
- package/dist/services/python.d.ts +14 -0
- package/dist/services/python.js +81 -0
- package/dist/services/system.d.ts +2 -0
- package/dist/services/system.js +22 -0
- package/dist/types/index.d.ts +82 -0
- package/dist/types/index.js +26 -0
- package/package.json +37 -0
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export interface CheckResult {
|
|
2
|
+
ok: boolean;
|
|
3
|
+
version?: string;
|
|
4
|
+
error?: string;
|
|
5
|
+
}
|
|
6
|
+
export declare function checkPython(): Promise<CheckResult>;
|
|
7
|
+
export declare function checkPip(): Promise<CheckResult>;
|
|
8
|
+
export declare function checkVenv(): Promise<CheckResult>;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { execFile } from 'node:child_process';
|
|
2
|
+
function execFileAsync(cmd, args) {
|
|
3
|
+
return new Promise((resolve, reject) => {
|
|
4
|
+
execFile(cmd, args, (err, stdout, stderr) => {
|
|
5
|
+
if (err) {
|
|
6
|
+
reject(err);
|
|
7
|
+
return;
|
|
8
|
+
}
|
|
9
|
+
resolve({ stdout: String(stdout), stderr: String(stderr) });
|
|
10
|
+
});
|
|
11
|
+
});
|
|
12
|
+
}
|
|
13
|
+
async function tryCommand(cmd, args) {
|
|
14
|
+
try {
|
|
15
|
+
const { stdout, stderr } = await execFileAsync(cmd, args);
|
|
16
|
+
const output = (stdout || stderr).trim();
|
|
17
|
+
const versionMatch = output.match(/\d+\.\d+\.\d+/);
|
|
18
|
+
return { ok: true, version: versionMatch?.[0] };
|
|
19
|
+
}
|
|
20
|
+
catch (err) {
|
|
21
|
+
const error = err;
|
|
22
|
+
return { ok: false, error: error.message ?? String(err) };
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
export async function checkPython() {
|
|
26
|
+
const result = await tryCommand('python3', ['--version']);
|
|
27
|
+
if (result.ok)
|
|
28
|
+
return result;
|
|
29
|
+
return tryCommand('python', ['--version']);
|
|
30
|
+
}
|
|
31
|
+
export async function checkPip() {
|
|
32
|
+
const result = await tryCommand('pip3', ['--version']);
|
|
33
|
+
if (result.ok)
|
|
34
|
+
return result;
|
|
35
|
+
return tryCommand('pip', ['--version']);
|
|
36
|
+
}
|
|
37
|
+
export async function checkVenv() {
|
|
38
|
+
const result = await tryCommand('python3', ['-m', 'venv', '--help']);
|
|
39
|
+
if (result.ok)
|
|
40
|
+
return { ok: true };
|
|
41
|
+
const result2 = await tryCommand('python', ['-m', 'venv', '--help']);
|
|
42
|
+
if (result2.ok)
|
|
43
|
+
return { ok: true };
|
|
44
|
+
const hint = process.platform === 'darwin'
|
|
45
|
+
? 'réinstallez Python via python.org ou brew install python3'
|
|
46
|
+
: 'sudo apt install python3-venv';
|
|
47
|
+
return { ok: false, error: `python venv non disponible (${hint})` };
|
|
48
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { NupoConfig } from '../types/index.js';
|
|
2
|
+
declare function getConfigDir(): string;
|
|
3
|
+
declare function getConfigPath(): string;
|
|
4
|
+
export declare function configExists(): Promise<boolean>;
|
|
5
|
+
export declare function readConfig(): Promise<NupoConfig>;
|
|
6
|
+
export declare function writeConfig(config: NupoConfig): Promise<void>;
|
|
7
|
+
export declare function patchConfig(partial: Partial<NupoConfig>): Promise<void>;
|
|
8
|
+
export { getConfigPath, getConfigDir };
|
|
9
|
+
export declare function getBaseConfPath(): string;
|
|
10
|
+
export declare function ensureBaseConf(): Promise<void>;
|
|
11
|
+
export declare function readBaseConf(): Promise<string>;
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import { homedir } from 'os';
|
|
2
|
+
import { join } from 'path';
|
|
3
|
+
import { readFile, writeFile, mkdir, access, rename } from 'fs/promises';
|
|
4
|
+
import { DEFAULT_CONFIG } from '../types/index.js';
|
|
5
|
+
// Serialise all writes to prevent concurrent writeFile corruption
|
|
6
|
+
let writeQueue = Promise.resolve();
|
|
7
|
+
function getConfigDir() {
|
|
8
|
+
return join(process.env['NUPO_CONFIG_DIR'] ?? homedir(), '.nupo');
|
|
9
|
+
}
|
|
10
|
+
function getConfigPath() {
|
|
11
|
+
return join(getConfigDir(), 'config.json');
|
|
12
|
+
}
|
|
13
|
+
export async function configExists() {
|
|
14
|
+
try {
|
|
15
|
+
await access(getConfigPath());
|
|
16
|
+
return true;
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
return false;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
export async function readConfig() {
|
|
23
|
+
try {
|
|
24
|
+
const data = await readFile(getConfigPath(), 'utf-8');
|
|
25
|
+
return { ...DEFAULT_CONFIG, ...JSON.parse(data) };
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
return { ...DEFAULT_CONFIG };
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
export function writeConfig(config) {
|
|
32
|
+
writeQueue = writeQueue.then(async () => {
|
|
33
|
+
const dir = getConfigDir();
|
|
34
|
+
const dest = getConfigPath();
|
|
35
|
+
const tmp = dest + '.tmp';
|
|
36
|
+
await mkdir(dir, { recursive: true });
|
|
37
|
+
await writeFile(tmp, JSON.stringify(config, null, 2), 'utf-8');
|
|
38
|
+
await rename(tmp, dest);
|
|
39
|
+
});
|
|
40
|
+
return writeQueue;
|
|
41
|
+
}
|
|
42
|
+
export async function patchConfig(partial) {
|
|
43
|
+
const current = await readConfig();
|
|
44
|
+
await writeConfig({ ...current, ...partial });
|
|
45
|
+
}
|
|
46
|
+
export { getConfigPath, getConfigDir };
|
|
47
|
+
// ── Base Odoo conf ────────────────────────────────────────────────────────────
|
|
48
|
+
export function getBaseConfPath() {
|
|
49
|
+
return join(getConfigDir(), 'odoo_base.conf');
|
|
50
|
+
}
|
|
51
|
+
function getOdooDataDir() {
|
|
52
|
+
if (process.platform === 'darwin') {
|
|
53
|
+
return join(homedir(), 'Library', 'Application Support', 'Odoo');
|
|
54
|
+
}
|
|
55
|
+
return join(homedir(), '.local', 'share', 'Odoo');
|
|
56
|
+
}
|
|
57
|
+
function getGeoIpPath() {
|
|
58
|
+
if (process.platform === 'darwin') {
|
|
59
|
+
return process.arch === 'arm64'
|
|
60
|
+
? '/opt/homebrew/share/GeoIP/GeoLite2-City.mmdb'
|
|
61
|
+
: '/usr/local/share/GeoIP/GeoLite2-City.mmdb';
|
|
62
|
+
}
|
|
63
|
+
return '/usr/share/GeoIP/GeoLite2-City.mmdb';
|
|
64
|
+
}
|
|
65
|
+
function buildDefaultBaseConf() {
|
|
66
|
+
return `[options]
|
|
67
|
+
csv_internal_sep = ,
|
|
68
|
+
data_dir = ${getOdooDataDir()}
|
|
69
|
+
db_host = False
|
|
70
|
+
db_maxconn = 64
|
|
71
|
+
db_name = False
|
|
72
|
+
db_password = False
|
|
73
|
+
db_port = False
|
|
74
|
+
db_sslmode = prefer
|
|
75
|
+
db_template = template0
|
|
76
|
+
db_user = False
|
|
77
|
+
dbfilter =
|
|
78
|
+
demo = {}
|
|
79
|
+
email_from = False
|
|
80
|
+
from_filter = False
|
|
81
|
+
geoip_database = ${getGeoIpPath()}
|
|
82
|
+
gevent_port = 8077
|
|
83
|
+
http_enable = True
|
|
84
|
+
http_interface =
|
|
85
|
+
http_port = 8017
|
|
86
|
+
import_partial =
|
|
87
|
+
limit_memory_hard = 2684354560
|
|
88
|
+
limit_memory_soft = 2147483648
|
|
89
|
+
limit_request = 65536
|
|
90
|
+
limit_time_cpu = 6000
|
|
91
|
+
limit_time_real = 1200
|
|
92
|
+
limit_time_real_cron = -1
|
|
93
|
+
list_db = True
|
|
94
|
+
log_db = False
|
|
95
|
+
log_db_level = warning
|
|
96
|
+
log_handler = :INFO
|
|
97
|
+
log_level = info
|
|
98
|
+
logfile =
|
|
99
|
+
max_cron_threads = 2
|
|
100
|
+
osv_memory_age_limit = False
|
|
101
|
+
osv_memory_count_limit = 0
|
|
102
|
+
pg_path =
|
|
103
|
+
pidfile =
|
|
104
|
+
proxy_mode = False
|
|
105
|
+
reportgz = False
|
|
106
|
+
screencasts =
|
|
107
|
+
screenshots = /tmp/odoo_tests
|
|
108
|
+
server_wide_modules = base,web
|
|
109
|
+
smtp_password = False
|
|
110
|
+
smtp_port = 25
|
|
111
|
+
smtp_server = localhost
|
|
112
|
+
smtp_ssl = False
|
|
113
|
+
smtp_ssl_certificate_filename = False
|
|
114
|
+
smtp_ssl_private_key_filename = False
|
|
115
|
+
smtp_user = False
|
|
116
|
+
syslog = False
|
|
117
|
+
test_enable = False
|
|
118
|
+
test_file =
|
|
119
|
+
test_tags = None
|
|
120
|
+
transient_age_limit = 1.0
|
|
121
|
+
translate_modules = ['all']
|
|
122
|
+
unaccent = False
|
|
123
|
+
upgrade_path =
|
|
124
|
+
websocket_keep_alive_timeout = 3600
|
|
125
|
+
websocket_rate_limit_burst = 10
|
|
126
|
+
websocket_rate_limit_delay = 0.2
|
|
127
|
+
without_demo = False
|
|
128
|
+
workers = 0
|
|
129
|
+
x_sendfile = False
|
|
130
|
+
admin_passwd = admin
|
|
131
|
+
`;
|
|
132
|
+
}
|
|
133
|
+
export async function ensureBaseConf() {
|
|
134
|
+
const path = getBaseConfPath();
|
|
135
|
+
try {
|
|
136
|
+
await access(path);
|
|
137
|
+
}
|
|
138
|
+
catch {
|
|
139
|
+
await mkdir(getConfigDir(), { recursive: true });
|
|
140
|
+
await writeFile(path, buildDefaultBaseConf(), 'utf-8');
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
export async function readBaseConf() {
|
|
144
|
+
await ensureBaseConf();
|
|
145
|
+
return readFile(getBaseConfPath(), 'utf-8');
|
|
146
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
export declare const ODOO_COMMUNITY_URL = "https://github.com/odoo/odoo.git";
|
|
2
|
+
export declare const ODOO_ENTERPRISE_URL = "git@github.com:odoo/enterprise.git";
|
|
3
|
+
export interface GitResult {
|
|
4
|
+
ok: boolean;
|
|
5
|
+
error?: string;
|
|
6
|
+
}
|
|
7
|
+
export interface GitProgress {
|
|
8
|
+
phase: 'receiving' | 'resolving';
|
|
9
|
+
percent: number;
|
|
10
|
+
speed?: string;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Check whether a branch exists on a remote git repository.
|
|
14
|
+
* Uses `git ls-remote` — no clone required.
|
|
15
|
+
*/
|
|
16
|
+
export declare function checkBranch(repoUrl: string, branch: string): Promise<GitResult>;
|
|
17
|
+
/**
|
|
18
|
+
* Get the HEAD commit hash of a local git repository.
|
|
19
|
+
*/
|
|
20
|
+
export declare function getLocalCommit(repoPath: string): Promise<string | null>;
|
|
21
|
+
/**
|
|
22
|
+
* Get the HEAD commit hash for a branch on a remote (via ls-remote).
|
|
23
|
+
*/
|
|
24
|
+
export declare function getRemoteCommit(repoUrl: string, branch: string): Promise<string | null>;
|
|
25
|
+
/**
|
|
26
|
+
* Update a local shallow clone:
|
|
27
|
+
* git fetch --depth 1 --progress origin <branch> (stderr streamed for progress)
|
|
28
|
+
* git reset --hard origin/<branch>
|
|
29
|
+
*/
|
|
30
|
+
export declare function updateRepo(repoPath: string, branch: string, onProgress?: (progress: GitProgress) => void): Promise<GitResult>;
|
|
31
|
+
/**
|
|
32
|
+
* Clone a repository with --depth 1 (no full history).
|
|
33
|
+
* Streams stderr in real time to call onProgress with parsed progress info.
|
|
34
|
+
*/
|
|
35
|
+
export declare function cloneRepo(url: string, dest: string, branch: string, onProgress?: (progress: GitProgress) => void): Promise<GitResult>;
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import { execFile, spawn } from 'node:child_process';
|
|
2
|
+
export const ODOO_COMMUNITY_URL = 'https://github.com/odoo/odoo.git';
|
|
3
|
+
// Enterprise is a private repo: use SSH so the user's key is used automatically.
|
|
4
|
+
export const ODOO_ENTERPRISE_URL = 'git@github.com:odoo/enterprise.git';
|
|
5
|
+
// GIT_TERMINAL_PROMPT=0 prevents git from hanging waiting for credential input
|
|
6
|
+
// when there is no TTY (execFile / spawn have no TTY by default).
|
|
7
|
+
// LC_ALL=C forces English output so progress line parsing works regardless of system locale.
|
|
8
|
+
const GIT_ENV = { ...process.env, GIT_TERMINAL_PROMPT: '0', LC_ALL: 'C' };
|
|
9
|
+
function execFileAsync(cmd, args, maxBuffer = 1024 * 1024, options = {}) {
|
|
10
|
+
return new Promise((resolve, reject) => {
|
|
11
|
+
execFile(cmd, args, { maxBuffer, env: GIT_ENV, cwd: options.cwd }, (err, stdout, stderr) => {
|
|
12
|
+
if (err) {
|
|
13
|
+
reject(err);
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
resolve({ stdout: String(stdout), stderr: String(stderr) });
|
|
17
|
+
});
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Parse a single git progress line into a structured object.
|
|
22
|
+
* Git writes lines like:
|
|
23
|
+
* "Receiving objects: 45% (5555/12345), 10.23 MiB | 3.14 MiB/s"
|
|
24
|
+
* "Resolving deltas: 67% (1234/1844)"
|
|
25
|
+
*/
|
|
26
|
+
function parseGitProgress(line) {
|
|
27
|
+
const m = line.match(/^(Receiving objects|Resolving deltas):\s+(\d+)%[^|]*(?:\|\s*([\d.]+ \S+\/s))?/);
|
|
28
|
+
if (!m)
|
|
29
|
+
return null;
|
|
30
|
+
return {
|
|
31
|
+
phase: m[1] === 'Receiving objects' ? 'receiving' : 'resolving',
|
|
32
|
+
percent: parseInt(m[2], 10),
|
|
33
|
+
speed: m[3]?.trim(),
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Check whether a branch exists on a remote git repository.
|
|
38
|
+
* Uses `git ls-remote` — no clone required.
|
|
39
|
+
*/
|
|
40
|
+
export async function checkBranch(repoUrl, branch) {
|
|
41
|
+
try {
|
|
42
|
+
const { stdout } = await execFileAsync('git', [
|
|
43
|
+
'ls-remote', '--heads', repoUrl, `refs/heads/${branch}`,
|
|
44
|
+
]);
|
|
45
|
+
if (stdout.trim())
|
|
46
|
+
return { ok: true };
|
|
47
|
+
return { ok: false, error: `Branche "${branch}" introuvable dans le dépôt` };
|
|
48
|
+
}
|
|
49
|
+
catch (err) {
|
|
50
|
+
return { ok: false, error: err.message ?? String(err) };
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Get the HEAD commit hash of a local git repository.
|
|
55
|
+
*/
|
|
56
|
+
export async function getLocalCommit(repoPath) {
|
|
57
|
+
try {
|
|
58
|
+
const { stdout } = await execFileAsync('git', ['rev-parse', 'HEAD'], 1024 * 1024, { cwd: repoPath });
|
|
59
|
+
return stdout.trim() || null;
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Get the HEAD commit hash for a branch on a remote (via ls-remote).
|
|
67
|
+
*/
|
|
68
|
+
export async function getRemoteCommit(repoUrl, branch) {
|
|
69
|
+
try {
|
|
70
|
+
const { stdout } = await execFileAsync('git', ['ls-remote', repoUrl, `refs/heads/${branch}`]);
|
|
71
|
+
const hash = stdout.trim().split(/[\t\s]/)[0] ?? '';
|
|
72
|
+
return hash || null;
|
|
73
|
+
}
|
|
74
|
+
catch {
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Update a local shallow clone:
|
|
80
|
+
* git fetch --depth 1 --progress origin <branch> (stderr streamed for progress)
|
|
81
|
+
* git reset --hard origin/<branch>
|
|
82
|
+
*/
|
|
83
|
+
export function updateRepo(repoPath, branch, onProgress) {
|
|
84
|
+
return new Promise(resolve => {
|
|
85
|
+
const fetchProc = spawn('git', ['fetch', '--depth', '1', '--progress', 'origin', branch], { env: GIT_ENV, stdio: ['ignore', 'pipe', 'pipe'], cwd: repoPath });
|
|
86
|
+
let stderrFull = '';
|
|
87
|
+
fetchProc.stderr?.on('data', (chunk) => {
|
|
88
|
+
const text = chunk.toString();
|
|
89
|
+
stderrFull += text;
|
|
90
|
+
for (const part of text.split(/[\r\n]/)) {
|
|
91
|
+
const line = part.trim();
|
|
92
|
+
if (!line)
|
|
93
|
+
continue;
|
|
94
|
+
const progress = parseGitProgress(line);
|
|
95
|
+
if (progress)
|
|
96
|
+
onProgress?.(progress);
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
fetchProc.stdout?.resume();
|
|
100
|
+
let settled = false;
|
|
101
|
+
const done = (result) => {
|
|
102
|
+
if (!settled) {
|
|
103
|
+
settled = true;
|
|
104
|
+
resolve(result);
|
|
105
|
+
}
|
|
106
|
+
};
|
|
107
|
+
fetchProc.on('error', err => done({ ok: false, error: err.message }));
|
|
108
|
+
fetchProc.on('close', code => {
|
|
109
|
+
if (code !== 0) {
|
|
110
|
+
const errorMsg = stderrFull.split('\n').map(l => l.trim()).filter(l => /^(fatal|error):/i.test(l)).join(' · ') ||
|
|
111
|
+
stderrFull.trim().split('\n').pop()?.trim() ||
|
|
112
|
+
'Fetch échoué';
|
|
113
|
+
done({ ok: false, error: errorMsg });
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
// fetch OK → reset --hard
|
|
117
|
+
execFileAsync('git', ['reset', '--hard', `origin/${branch}`], 1024 * 1024, { cwd: repoPath })
|
|
118
|
+
.then(() => done({ ok: true }))
|
|
119
|
+
.catch(err => done({ ok: false, error: err.message }));
|
|
120
|
+
});
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Clone a repository with --depth 1 (no full history).
|
|
125
|
+
* Streams stderr in real time to call onProgress with parsed progress info.
|
|
126
|
+
*/
|
|
127
|
+
export function cloneRepo(url, dest, branch, onProgress) {
|
|
128
|
+
return new Promise(resolve => {
|
|
129
|
+
const proc = spawn('git', ['clone', '--depth', '1', '--branch', branch, '--progress', url, dest], { env: GIT_ENV, stdio: ['ignore', 'pipe', 'pipe'] });
|
|
130
|
+
// Collect stderr for error extraction; parse progress in real time
|
|
131
|
+
let stderrFull = '';
|
|
132
|
+
proc.stderr?.on('data', (chunk) => {
|
|
133
|
+
const text = chunk.toString();
|
|
134
|
+
stderrFull += text;
|
|
135
|
+
// Git uses \r to overwrite progress lines on the same terminal line.
|
|
136
|
+
// Split on both \r and \n to get individual lines.
|
|
137
|
+
for (const part of text.split(/[\r\n]/)) {
|
|
138
|
+
const line = part.trim();
|
|
139
|
+
if (!line)
|
|
140
|
+
continue;
|
|
141
|
+
const progress = parseGitProgress(line);
|
|
142
|
+
if (progress)
|
|
143
|
+
onProgress?.(progress);
|
|
144
|
+
}
|
|
145
|
+
});
|
|
146
|
+
// stdout is usually empty for git clone; drain it to avoid backpressure
|
|
147
|
+
proc.stdout?.resume();
|
|
148
|
+
let settled = false;
|
|
149
|
+
const done = (result) => {
|
|
150
|
+
if (!settled) {
|
|
151
|
+
settled = true;
|
|
152
|
+
resolve(result);
|
|
153
|
+
}
|
|
154
|
+
};
|
|
155
|
+
proc.on('error', err => done({ ok: false, error: err.message }));
|
|
156
|
+
proc.on('close', code => {
|
|
157
|
+
if (code === 0) {
|
|
158
|
+
done({ ok: true });
|
|
159
|
+
}
|
|
160
|
+
else {
|
|
161
|
+
// Extract the most meaningful line from stderr (fatal: / error: lines)
|
|
162
|
+
const errorMsg = stderrFull
|
|
163
|
+
.split('\n')
|
|
164
|
+
.map(l => l.trim())
|
|
165
|
+
.filter(l => /^(fatal|error):/i.test(l))
|
|
166
|
+
.join(' · ') ||
|
|
167
|
+
stderrFull.trim().split('\n').pop()?.trim() ||
|
|
168
|
+
'Clone échoué';
|
|
169
|
+
done({ ok: false, error: errorMsg });
|
|
170
|
+
}
|
|
171
|
+
});
|
|
172
|
+
});
|
|
173
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { OdooVersion, OdooServiceConfig } from '../types/index.js';
|
|
2
|
+
export type IdeStepId = 'vscode_dir' | 'settings_json' | 'launch_json' | 'open_vscode';
|
|
3
|
+
export type IdeStepStatus = 'running' | 'success' | 'error';
|
|
4
|
+
export type IdeStepCallback = (id: IdeStepId, status: IdeStepStatus, detail?: string) => void;
|
|
5
|
+
/**
|
|
6
|
+
* Sets up .vscode/ for the given Odoo version and opens VS Code.
|
|
7
|
+
* Calls onStep for each step so callers can display progress (TUI or CLI).
|
|
8
|
+
* Returns true on success, false on any error.
|
|
9
|
+
*/
|
|
10
|
+
export declare function setupVsCode(version: OdooVersion, services: OdooServiceConfig[], onStep: IdeStepCallback): Promise<boolean>;
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { mkdir, access, readFile, writeFile } from 'fs/promises';
|
|
2
|
+
import { join } from 'path';
|
|
3
|
+
import { spawn } from 'child_process';
|
|
4
|
+
function buildAddonsPaths(service) {
|
|
5
|
+
const paths = [join(service.versionPath, 'community', 'addons')];
|
|
6
|
+
if (service.useEnterprise)
|
|
7
|
+
paths.push(join(service.versionPath, 'enterprise'));
|
|
8
|
+
for (const f of service.customFolders)
|
|
9
|
+
paths.push(join(service.versionPath, 'custom', f));
|
|
10
|
+
return paths;
|
|
11
|
+
}
|
|
12
|
+
function buildDebugConfig(service) {
|
|
13
|
+
return {
|
|
14
|
+
name: service.name,
|
|
15
|
+
type: 'debugpy',
|
|
16
|
+
request: 'launch',
|
|
17
|
+
stopOnEntry: false,
|
|
18
|
+
program: join(service.versionPath, 'community', 'odoo-bin'),
|
|
19
|
+
args: [
|
|
20
|
+
'--addons-path',
|
|
21
|
+
buildAddonsPaths(service).join(','),
|
|
22
|
+
'-c',
|
|
23
|
+
service.confPath,
|
|
24
|
+
],
|
|
25
|
+
console: 'integratedTerminal',
|
|
26
|
+
justMyCode: true,
|
|
27
|
+
env: {
|
|
28
|
+
PYTHONPATH: '${workspaceFolder}',
|
|
29
|
+
},
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
// ── Main setup function ───────────────────────────────────────────────────────
|
|
33
|
+
/**
|
|
34
|
+
* Sets up .vscode/ for the given Odoo version and opens VS Code.
|
|
35
|
+
* Calls onStep for each step so callers can display progress (TUI or CLI).
|
|
36
|
+
* Returns true on success, false on any error.
|
|
37
|
+
*/
|
|
38
|
+
export async function setupVsCode(version, services, onStep) {
|
|
39
|
+
const vscodeDir = join(version.path, '.vscode');
|
|
40
|
+
const settingsPath = join(vscodeDir, 'settings.json');
|
|
41
|
+
const launchPath = join(vscodeDir, 'launch.json');
|
|
42
|
+
// ── Step 1: .vscode directory ───────────────────────────────────────────────
|
|
43
|
+
onStep('vscode_dir', 'running');
|
|
44
|
+
try {
|
|
45
|
+
await mkdir(vscodeDir, { recursive: true });
|
|
46
|
+
onStep('vscode_dir', 'success');
|
|
47
|
+
}
|
|
48
|
+
catch (e) {
|
|
49
|
+
onStep('vscode_dir', 'error', String(e));
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
// ── Step 2: settings.json ───────────────────────────────────────────────────
|
|
53
|
+
onStep('settings_json', 'running');
|
|
54
|
+
try {
|
|
55
|
+
let exists = false;
|
|
56
|
+
try {
|
|
57
|
+
await access(settingsPath);
|
|
58
|
+
exists = true;
|
|
59
|
+
}
|
|
60
|
+
catch { }
|
|
61
|
+
if (!exists) {
|
|
62
|
+
const content = {
|
|
63
|
+
'python.defaultInterpreterPath': '${workspaceFolder}/.venv/bin/python',
|
|
64
|
+
'python.terminal.activateEnvironment': true,
|
|
65
|
+
};
|
|
66
|
+
await writeFile(settingsPath, JSON.stringify(content, null, 4), 'utf-8');
|
|
67
|
+
onStep('settings_json', 'success', 'créé');
|
|
68
|
+
}
|
|
69
|
+
else {
|
|
70
|
+
onStep('settings_json', 'success', 'existant');
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
catch (e) {
|
|
74
|
+
onStep('settings_json', 'error', String(e));
|
|
75
|
+
return false;
|
|
76
|
+
}
|
|
77
|
+
// ── Step 3: launch.json ─────────────────────────────────────────────────────
|
|
78
|
+
onStep('launch_json', 'running');
|
|
79
|
+
try {
|
|
80
|
+
let launchData = { version: '0.2.0', configurations: [] };
|
|
81
|
+
try {
|
|
82
|
+
const raw = await readFile(launchPath, 'utf-8');
|
|
83
|
+
const parsed = JSON.parse(raw);
|
|
84
|
+
launchData.version = parsed.version ?? '0.2.0';
|
|
85
|
+
launchData.configurations = Array.isArray(parsed.configurations) ? parsed.configurations : [];
|
|
86
|
+
}
|
|
87
|
+
catch { }
|
|
88
|
+
const versionServices = services.filter(s => s.versionPath === version.path);
|
|
89
|
+
let added = 0;
|
|
90
|
+
for (const svc of versionServices) {
|
|
91
|
+
const alreadyExists = launchData.configurations.some(c => c.name === svc.name);
|
|
92
|
+
if (!alreadyExists) {
|
|
93
|
+
launchData.configurations.push(buildDebugConfig(svc));
|
|
94
|
+
added++;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
await writeFile(launchPath, JSON.stringify(launchData, null, 4), 'utf-8');
|
|
98
|
+
const parts = [];
|
|
99
|
+
if (added)
|
|
100
|
+
parts.push(`${added} config(s) ajoutée(s)`);
|
|
101
|
+
if (!added && versionServices.length)
|
|
102
|
+
parts.push('configs déjà présentes');
|
|
103
|
+
if (!versionServices.length)
|
|
104
|
+
parts.push('aucun service pour cette version');
|
|
105
|
+
onStep('launch_json', 'success', parts.join(', '));
|
|
106
|
+
}
|
|
107
|
+
catch (e) {
|
|
108
|
+
onStep('launch_json', 'error', String(e));
|
|
109
|
+
return false;
|
|
110
|
+
}
|
|
111
|
+
// ── Step 4: open VS Code ────────────────────────────────────────────────────
|
|
112
|
+
onStep('open_vscode', 'running');
|
|
113
|
+
try {
|
|
114
|
+
await new Promise((resolve, reject) => {
|
|
115
|
+
const proc = spawn('code', [version.path], { detached: true, stdio: 'ignore' });
|
|
116
|
+
proc.on('error', reject);
|
|
117
|
+
setTimeout(() => { proc.unref(); resolve(); }, 200);
|
|
118
|
+
});
|
|
119
|
+
onStep('open_vscode', 'success');
|
|
120
|
+
}
|
|
121
|
+
catch {
|
|
122
|
+
onStep('open_vscode', 'error', 'code introuvable — vérifiez le PATH');
|
|
123
|
+
return false;
|
|
124
|
+
}
|
|
125
|
+
return true;
|
|
126
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export interface PythonResult {
|
|
2
|
+
ok: boolean;
|
|
3
|
+
error?: string;
|
|
4
|
+
}
|
|
5
|
+
/**
|
|
6
|
+
* Create a Python virtual environment at the given path.
|
|
7
|
+
* Tries python3 first, then python.
|
|
8
|
+
*/
|
|
9
|
+
export declare function createVenv(venvPath: string): Promise<PythonResult>;
|
|
10
|
+
/**
|
|
11
|
+
* Install pip requirements from a requirements.txt file using the venv's pip.
|
|
12
|
+
* Streams stdout/stderr lines to onOutput for live feedback.
|
|
13
|
+
*/
|
|
14
|
+
export declare function installRequirements(pipPath: string, requirementsPath: string, onOutput?: (line: string) => void): Promise<PythonResult>;
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { execFile, spawn } from 'node:child_process';
|
|
2
|
+
function execFileAsync(cmd, args) {
|
|
3
|
+
return new Promise((resolve, reject) => {
|
|
4
|
+
execFile(cmd, args, { maxBuffer: 4 * 1024 * 1024 }, (err, stdout, stderr) => {
|
|
5
|
+
if (err) {
|
|
6
|
+
err.stderrOutput = String(stderr);
|
|
7
|
+
reject(err);
|
|
8
|
+
return;
|
|
9
|
+
}
|
|
10
|
+
resolve({ stdout: String(stdout), stderr: String(stderr) });
|
|
11
|
+
});
|
|
12
|
+
});
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Create a Python virtual environment at the given path.
|
|
16
|
+
* Tries python3 first, then python.
|
|
17
|
+
*/
|
|
18
|
+
export async function createVenv(venvPath) {
|
|
19
|
+
const candidates = ['python3', 'python'];
|
|
20
|
+
for (const cmd of candidates) {
|
|
21
|
+
try {
|
|
22
|
+
await execFileAsync(cmd, ['-m', 'venv', venvPath]);
|
|
23
|
+
return { ok: true };
|
|
24
|
+
}
|
|
25
|
+
catch (err) {
|
|
26
|
+
const e = err;
|
|
27
|
+
if (e.code === 'ENOENT')
|
|
28
|
+
continue; // command not found, try next
|
|
29
|
+
const detail = e.stderrOutput?.trim() || e.message;
|
|
30
|
+
return { ok: false, error: detail };
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return { ok: false, error: 'python3 / python introuvable sur ce système' };
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Install pip requirements from a requirements.txt file using the venv's pip.
|
|
37
|
+
* Streams stdout/stderr lines to onOutput for live feedback.
|
|
38
|
+
*/
|
|
39
|
+
export function installRequirements(pipPath, requirementsPath, onOutput) {
|
|
40
|
+
return new Promise(resolve => {
|
|
41
|
+
const proc = spawn(pipPath, ['install', '-r', requirementsPath], {
|
|
42
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
43
|
+
});
|
|
44
|
+
let stderrFull = '';
|
|
45
|
+
const handleChunk = (chunk) => {
|
|
46
|
+
for (const part of chunk.toString().split('\n')) {
|
|
47
|
+
const line = part.trim();
|
|
48
|
+
if (line)
|
|
49
|
+
onOutput?.(line);
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
proc.stdout?.on('data', handleChunk);
|
|
53
|
+
proc.stderr?.on('data', (chunk) => {
|
|
54
|
+
stderrFull += chunk.toString();
|
|
55
|
+
handleChunk(chunk);
|
|
56
|
+
});
|
|
57
|
+
let settled = false;
|
|
58
|
+
const done = (result) => {
|
|
59
|
+
if (!settled) {
|
|
60
|
+
settled = true;
|
|
61
|
+
resolve(result);
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
proc.on('error', err => done({ ok: false, error: err.message }));
|
|
65
|
+
proc.on('close', code => {
|
|
66
|
+
if (code === 0) {
|
|
67
|
+
done({ ok: true });
|
|
68
|
+
}
|
|
69
|
+
else {
|
|
70
|
+
const errMsg = stderrFull
|
|
71
|
+
.split('\n')
|
|
72
|
+
.map(l => l.trim())
|
|
73
|
+
.filter(l => /^(error|fatal):/i.test(l))
|
|
74
|
+
.join(' · ') ||
|
|
75
|
+
stderrFull.trim().split('\n').pop()?.trim() ||
|
|
76
|
+
'Installation des dépendances échouée';
|
|
77
|
+
done({ ok: false, error: errMsg });
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
}
|