@orion-monitoring/cli 1.0.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/README.md +51 -0
- package/dist/api.d.ts +12 -0
- package/dist/api.js +57 -0
- package/dist/auth-browser.d.ts +26 -0
- package/dist/auth-browser.js +233 -0
- package/dist/auth.d.ts +9 -0
- package/dist/auth.js +44 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +216 -0
- package/dist/types.d.ts +30 -0
- package/dist/types.js +1 -0
- package/dist/writer.d.ts +6 -0
- package/dist/writer.js +29 -0
- package/package.json +32 -0
package/README.md
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# orion-cli
|
|
2
|
+
|
|
3
|
+
CLI d'initialisation pour le SDK **Orion** — se connecte à ton instance Orion, récupère ou crée un projet, et génère `orion.config.ts`.
|
|
4
|
+
|
|
5
|
+
## Usage
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npx orion-cli
|
|
9
|
+
# ou
|
|
10
|
+
npx create-orion
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Flow
|
|
14
|
+
|
|
15
|
+
```
|
|
16
|
+
2. Login avec ton compte (email + mot de passe)
|
|
17
|
+
3. Sélectionner un projet (ou en créer un nouveau)
|
|
18
|
+
4. Nom de la source (ex: api-backend, worker-queue)
|
|
19
|
+
5. Environnement (production / development / staging / test)
|
|
20
|
+
6. Génère nom.config.ts (token dans .env ou directement)
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Fichier généré
|
|
24
|
+
|
|
25
|
+
```ts
|
|
26
|
+
// nom.config.ts
|
|
27
|
+
import { defineConfig } from 'orion-cli'
|
|
28
|
+
|
|
29
|
+
export default defineConfig({
|
|
30
|
+
token: process.env.ORION_TOKEN!, // ou le token directement
|
|
31
|
+
source: 'api-backend',
|
|
32
|
+
environment: 'production',
|
|
33
|
+
serverUrl: 'wss://api.monorion.com',
|
|
34
|
+
})
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Variable d'environnement
|
|
38
|
+
|
|
39
|
+
| Variable | Description |
|
|
40
|
+
|------------------|------------------------------------------|
|
|
41
|
+
| `ORION_API_URL` | Pré-remplit l'URL du serveur au prompt |
|
|
42
|
+
| `ORION_TOKEN` | Token injecté si stocké dans `.env` |
|
|
43
|
+
|
|
44
|
+
## Dev
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
npm install
|
|
48
|
+
npm run dev # tsx watch
|
|
49
|
+
npm run build # compile vers dist/
|
|
50
|
+
npm link # teste `orion-cli` en global
|
|
51
|
+
```
|
package/dist/api.d.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { LoginResponse, Project, CreateProjectResponse } from './types.js';
|
|
2
|
+
export declare class ApiError extends Error {
|
|
3
|
+
status: number;
|
|
4
|
+
constructor(status: number, message: string);
|
|
5
|
+
}
|
|
6
|
+
export declare function login(baseUrl: string, email: string, password: string): Promise<LoginResponse>;
|
|
7
|
+
export declare function listProjects(baseUrl: string, token: string): Promise<Project[]>;
|
|
8
|
+
export declare function createProject(baseUrl: string, token: string, name: string, label: string): Promise<CreateProjectResponse>;
|
|
9
|
+
export declare function getProjectToken(baseUrl: string, token: string, projectName: string): Promise<string>;
|
|
10
|
+
export declare function registerSource(baseUrl: string, token: string, projectName: string, name: string, description: string, env: string): Promise<{
|
|
11
|
+
name: string;
|
|
12
|
+
}>;
|
package/dist/api.js
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
export class ApiError extends Error {
|
|
2
|
+
status;
|
|
3
|
+
constructor(status, message) {
|
|
4
|
+
super(message);
|
|
5
|
+
this.status = status;
|
|
6
|
+
this.name = 'ApiError';
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
async function request(baseUrl, path, options = {}) {
|
|
10
|
+
const { token, ...fetchOptions } = options;
|
|
11
|
+
const headers = {
|
|
12
|
+
'Content-Type': 'application/json',
|
|
13
|
+
...(fetchOptions.headers ?? {}),
|
|
14
|
+
};
|
|
15
|
+
if (token)
|
|
16
|
+
headers['Authorization'] = `Bearer ${token}`;
|
|
17
|
+
const res = await fetch(`${baseUrl}${path}`, {
|
|
18
|
+
...fetchOptions,
|
|
19
|
+
headers,
|
|
20
|
+
signal: AbortSignal.timeout(10_000),
|
|
21
|
+
});
|
|
22
|
+
const data = await res.json().catch(() => ({}));
|
|
23
|
+
if (!res.ok) {
|
|
24
|
+
const message = data.message ?? `HTTP ${res.status}`;
|
|
25
|
+
throw new ApiError(res.status, message);
|
|
26
|
+
}
|
|
27
|
+
return data;
|
|
28
|
+
}
|
|
29
|
+
// ─── Auth ─────────────────────────────────────────────────────────────────────
|
|
30
|
+
export async function login(baseUrl, email, password) {
|
|
31
|
+
return request(baseUrl, '/api/auth/login', {
|
|
32
|
+
method: 'POST',
|
|
33
|
+
body: JSON.stringify({ email, password }),
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
// ─── Projects ─────────────────────────────────────────────────────────────────
|
|
37
|
+
export async function listProjects(baseUrl, token) {
|
|
38
|
+
return request(baseUrl, '/api/projects', { token });
|
|
39
|
+
}
|
|
40
|
+
export async function createProject(baseUrl, token, name, label) {
|
|
41
|
+
return request(baseUrl, '/api/projects', {
|
|
42
|
+
method: 'POST',
|
|
43
|
+
token,
|
|
44
|
+
body: JSON.stringify({ name, label }),
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
export async function getProjectToken(baseUrl, token, projectName) {
|
|
48
|
+
const res = await request(baseUrl, `/api/projects/${encodeURIComponent(projectName)}/token`, { token });
|
|
49
|
+
return res.token;
|
|
50
|
+
}
|
|
51
|
+
export async function registerSource(baseUrl, token, projectName, name, description, env) {
|
|
52
|
+
return request(baseUrl, `/api/projects/${encodeURIComponent(projectName)}/sources`, {
|
|
53
|
+
method: 'POST',
|
|
54
|
+
token,
|
|
55
|
+
body: JSON.stringify({ name, description, environment: env }),
|
|
56
|
+
});
|
|
57
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* auth-browser.ts
|
|
3
|
+
*
|
|
4
|
+
* Ce module gère le flow d'authentification CLI via navigateur.
|
|
5
|
+
*
|
|
6
|
+
* POURQUOI un serveur HTTP local ?
|
|
7
|
+
* Le navigateur ne peut pas "écrire" dans le terminal directement.
|
|
8
|
+
* La seule façon pour lui de transmettre une donnée au CLI est
|
|
9
|
+
* de faire une requête HTTP. On ouvre donc un mini serveur sur
|
|
10
|
+
* localhost:7777 qui écoute pendant max 5 minutes.
|
|
11
|
+
*
|
|
12
|
+
* POURQUOI le module `http` natif ?
|
|
13
|
+
* Pour ne pas ajouter de dépendance (express, etc.) à un CLI léger.
|
|
14
|
+
* Le module natif suffit pour une seule route GET /callback.
|
|
15
|
+
*
|
|
16
|
+
* DÉPENDANCE NÉCESSAIRE : `open`
|
|
17
|
+
* npm install open
|
|
18
|
+
* (ouvre le navigateur par défaut sur macOS, Linux, Windows)
|
|
19
|
+
*/
|
|
20
|
+
/**
|
|
21
|
+
* Lance le flow d'auth via navigateur.
|
|
22
|
+
*
|
|
23
|
+
* @param apiBase - URL de base de l'API (ex: "http://localhost:3001/")
|
|
24
|
+
* @returns Le JWT que le CLI pourra utiliser pour les appels API
|
|
25
|
+
*/
|
|
26
|
+
export declare function loginWithBrowser(apiBase: string): Promise<string>;
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* auth-browser.ts
|
|
3
|
+
*
|
|
4
|
+
* Ce module gère le flow d'authentification CLI via navigateur.
|
|
5
|
+
*
|
|
6
|
+
* POURQUOI un serveur HTTP local ?
|
|
7
|
+
* Le navigateur ne peut pas "écrire" dans le terminal directement.
|
|
8
|
+
* La seule façon pour lui de transmettre une donnée au CLI est
|
|
9
|
+
* de faire une requête HTTP. On ouvre donc un mini serveur sur
|
|
10
|
+
* localhost:7777 qui écoute pendant max 5 minutes.
|
|
11
|
+
*
|
|
12
|
+
* POURQUOI le module `http` natif ?
|
|
13
|
+
* Pour ne pas ajouter de dépendance (express, etc.) à un CLI léger.
|
|
14
|
+
* Le module natif suffit pour une seule route GET /callback.
|
|
15
|
+
*
|
|
16
|
+
* DÉPENDANCE NÉCESSAIRE : `open`
|
|
17
|
+
* npm install open
|
|
18
|
+
* (ouvre le navigateur par défaut sur macOS, Linux, Windows)
|
|
19
|
+
*/
|
|
20
|
+
import http from 'http';
|
|
21
|
+
import { URL } from 'url';
|
|
22
|
+
import { spinner, log } from '@clack/prompts';
|
|
23
|
+
import pc from 'picocolors';
|
|
24
|
+
// Port fixe sur lequel le CLI écoute le callback
|
|
25
|
+
// Ce port doit être whitelisté dans le CORS du backend si besoin
|
|
26
|
+
const CALLBACK_PORT = 7777;
|
|
27
|
+
// Timeout de 5 minutes max pour que l'user se connecte dans le navigateur
|
|
28
|
+
const AUTH_TIMEOUT_MS = 5 * 60 * 1000;
|
|
29
|
+
/**
|
|
30
|
+
* Lance le flow d'auth via navigateur.
|
|
31
|
+
*
|
|
32
|
+
* @param apiBase - URL de base de l'API (ex: "http://localhost:3001/")
|
|
33
|
+
* @returns Le JWT que le CLI pourra utiliser pour les appels API
|
|
34
|
+
*/
|
|
35
|
+
export async function loginWithBrowser(apiBase) {
|
|
36
|
+
// ── ÉTAPE 1 : Appelle /api/auth/cli/init ─────────────────────────────────
|
|
37
|
+
// On envoie notre port de callback au backend.
|
|
38
|
+
// Le backend génère un state UUID et nous retourne l'URL de login.
|
|
39
|
+
const initRes = await fetch(`${apiBase}/api/auth/cli/init`, {
|
|
40
|
+
method: 'POST',
|
|
41
|
+
headers: { 'Content-Type': 'application/json' },
|
|
42
|
+
body: JSON.stringify({ callbackPort: CALLBACK_PORT }),
|
|
43
|
+
});
|
|
44
|
+
if (!initRes.ok) {
|
|
45
|
+
throw new Error(`Erreur lors de l'initialisation (${initRes.status})`);
|
|
46
|
+
}
|
|
47
|
+
const { loginUrl } = await initRes.json();
|
|
48
|
+
// ── ÉTAPE 2 : Ouvre le navigateur ─────────────────────────────────────────
|
|
49
|
+
// `open` est un package qui appelle `xdg-open` (Linux), `open` (macOS),
|
|
50
|
+
// ou `start` (Windows) selon la plateforme.
|
|
51
|
+
log.info(pc.cyan(`Ouverture du navigateur... si le navigateur ne s'ouvre pas, copiez cette URL :\n ${loginUrl}`));
|
|
52
|
+
const { default: open } = await import('open');
|
|
53
|
+
await open(loginUrl);
|
|
54
|
+
// ── ÉTAPE 3 : Démarre le serveur local et attend le token ─────────────────
|
|
55
|
+
// Le website va rediriger vers http://localhost:7777/callback?token=xxx
|
|
56
|
+
// Notre serveur intercepte ça, extrait le token, et se ferme.
|
|
57
|
+
const spin = spinner();
|
|
58
|
+
spin.start('En attente de l\'authentification dans le navigateur');
|
|
59
|
+
const token = await waitForCallback();
|
|
60
|
+
spin.stop(pc.green('✓ Authentification réussie !'));
|
|
61
|
+
return token;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Démarre un serveur HTTP local sur CALLBACK_PORT,
|
|
65
|
+
* attend un GET /callback?token=xxx,
|
|
66
|
+
* retourne le token et ferme le serveur.
|
|
67
|
+
*
|
|
68
|
+
* Timeout automatique après AUTH_TIMEOUT_MS.
|
|
69
|
+
*/
|
|
70
|
+
function waitForCallback() {
|
|
71
|
+
return new Promise((resolve, reject) => {
|
|
72
|
+
const server = http.createServer((req, res) => {
|
|
73
|
+
// On parse l'URL pour extraire le ?token= param
|
|
74
|
+
const reqUrl = new URL(req.url ?? '/', `http://localhost:${CALLBACK_PORT}`);
|
|
75
|
+
if (reqUrl.pathname !== '/callback') {
|
|
76
|
+
// Route inconnue → on ignore
|
|
77
|
+
res.writeHead(404);
|
|
78
|
+
res.end();
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
const token = reqUrl.searchParams.get('token');
|
|
82
|
+
if (!token) {
|
|
83
|
+
// Pas de token dans l'URL → erreur
|
|
84
|
+
res.writeHead(400, { 'Content-Type': 'text/html; charset=utf-8' });
|
|
85
|
+
res.end('<h2>Token manquant. Relancez orion-cli.</h2>');
|
|
86
|
+
server.close();
|
|
87
|
+
reject(new Error('Token manquant dans le callback'));
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
// ✅ On a le token ! On répond au navigateur avec une belle page
|
|
91
|
+
// et on résout la Promise.
|
|
92
|
+
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
|
|
93
|
+
res.end(`<!DOCTYPE html>
|
|
94
|
+
<html lang="fr">
|
|
95
|
+
<head>
|
|
96
|
+
<meta charset="UTF-8">
|
|
97
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
98
|
+
<title>Orion CLI — Authentifié</title>
|
|
99
|
+
<style>
|
|
100
|
+
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
|
101
|
+
|
|
102
|
+
body {
|
|
103
|
+
font-family: system-ui, -apple-system, sans-serif;
|
|
104
|
+
background: #161a24;
|
|
105
|
+
color: #e8eaef;
|
|
106
|
+
display: flex;
|
|
107
|
+
align-items: center;
|
|
108
|
+
justify-content: center;
|
|
109
|
+
min-height: 100vh;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
.card {
|
|
113
|
+
width: 100%;
|
|
114
|
+
max-width: 22rem;
|
|
115
|
+
background: #1c2130;
|
|
116
|
+
border: 1px solid #252b3b;
|
|
117
|
+
border-radius: 1rem;
|
|
118
|
+
padding: 2.5rem 2rem;
|
|
119
|
+
text-align: center;
|
|
120
|
+
box-shadow: 0 25px 50px rgba(0, 0, 0, 0.5);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
.logo {
|
|
124
|
+
font-size: 1.5rem;
|
|
125
|
+
font-weight: 700;
|
|
126
|
+
color: #ffffff;
|
|
127
|
+
letter-spacing: -0.02em;
|
|
128
|
+
margin-bottom: 0.25rem;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
.subtitle {
|
|
132
|
+
font-size: 0.875rem;
|
|
133
|
+
color: #8b92a4;
|
|
134
|
+
margin-bottom: 2rem;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
.icon-wrap {
|
|
138
|
+
width: 3.5rem;
|
|
139
|
+
height: 3.5rem;
|
|
140
|
+
background: rgba(2, 241, 148, 0.1);
|
|
141
|
+
border: 1px solid rgba(2, 241, 148, 0.25);
|
|
142
|
+
border-radius: 50%;
|
|
143
|
+
display: flex;
|
|
144
|
+
align-items: center;
|
|
145
|
+
justify-content: center;
|
|
146
|
+
margin: 0 auto 1.25rem;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
.icon-wrap svg {
|
|
150
|
+
color: #02f194;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
h2 {
|
|
154
|
+
font-size: 1.125rem;
|
|
155
|
+
font-weight: 600;
|
|
156
|
+
color: #ffffff;
|
|
157
|
+
margin-bottom: 0.5rem;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
p {
|
|
161
|
+
font-size: 0.875rem;
|
|
162
|
+
color: #8b92a4;
|
|
163
|
+
line-height: 1.5;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
.divider {
|
|
167
|
+
height: 1px;
|
|
168
|
+
background: #252b3b;
|
|
169
|
+
margin: 1.5rem 0;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
.hint {
|
|
173
|
+
font-size: 0.8125rem;
|
|
174
|
+
color: #8b92a4;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
.hint code {
|
|
178
|
+
color: #02f194;
|
|
179
|
+
background: rgba(2, 241, 148, 0.08);
|
|
180
|
+
padding: 0.125rem 0.375rem;
|
|
181
|
+
border-radius: 0.25rem;
|
|
182
|
+
font-family: ui-monospace, monospace;
|
|
183
|
+
font-size: 0.8125rem;
|
|
184
|
+
}
|
|
185
|
+
</style>
|
|
186
|
+
</head>
|
|
187
|
+
<body>
|
|
188
|
+
<div class="card">
|
|
189
|
+
<p class="logo">Orion</p>
|
|
190
|
+
<p class="subtitle">Authentification CLI</p>
|
|
191
|
+
|
|
192
|
+
<div class="icon-wrap">
|
|
193
|
+
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
|
194
|
+
<polyline points="20 6 9 17 4 12"></polyline>
|
|
195
|
+
</svg>
|
|
196
|
+
</div>
|
|
197
|
+
|
|
198
|
+
<h2>Authentification réussie !</h2>
|
|
199
|
+
<p>Vous pouvez fermer cet onglet<br>et retourner dans le terminal.</p>
|
|
200
|
+
|
|
201
|
+
<div class="divider"></div>
|
|
202
|
+
|
|
203
|
+
<p class="hint">Le setup continue dans <code>orion-cli</code></p>
|
|
204
|
+
</div>
|
|
205
|
+
</body>
|
|
206
|
+
</html>`);
|
|
207
|
+
// Ferme le serveur proprement après avoir répondu
|
|
208
|
+
server.close();
|
|
209
|
+
resolve(token);
|
|
210
|
+
});
|
|
211
|
+
// Timeout de sécurité : si l'user ne se connecte pas dans les temps
|
|
212
|
+
const timeout = setTimeout(() => {
|
|
213
|
+
server.close();
|
|
214
|
+
reject(new Error('Timeout : aucune authentification reçue en 5 minutes.'));
|
|
215
|
+
}, AUTH_TIMEOUT_MS);
|
|
216
|
+
// Quand le serveur se ferme, on clear le timeout
|
|
217
|
+
server.on('close', () => clearTimeout(timeout));
|
|
218
|
+
// Lance l'écoute sur le port
|
|
219
|
+
server.listen(CALLBACK_PORT, '127.0.0.1', () => {
|
|
220
|
+
// Le serveur est prêt, le spinner peut afficher son message
|
|
221
|
+
});
|
|
222
|
+
// Gestion d'erreur si le port est déjà occupé
|
|
223
|
+
server.on('error', (err) => {
|
|
224
|
+
if (err.code === 'EADDRINUSE') {
|
|
225
|
+
reject(new Error(`Le port ${CALLBACK_PORT} est déjà utilisé.\n` +
|
|
226
|
+
`Fermez le processus qui l'utilise et relancez orion-cli.`));
|
|
227
|
+
}
|
|
228
|
+
else {
|
|
229
|
+
reject(err);
|
|
230
|
+
}
|
|
231
|
+
});
|
|
232
|
+
});
|
|
233
|
+
}
|
package/dist/auth.d.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { ApiVerifyResponse, NomConfig } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Vérifie le token auprès de l'API et retourne les infos du projet.
|
|
4
|
+
*/
|
|
5
|
+
export declare function verifyToken(token: string, apiUrl?: string): Promise<ApiVerifyResponse>;
|
|
6
|
+
/**
|
|
7
|
+
* Construit les headers d'authentification pour les requêtes SDK.
|
|
8
|
+
*/
|
|
9
|
+
export declare function buildAuthHeaders(config: Pick<NomConfig, 'token'>): Record<string, string>;
|
package/dist/auth.js
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
// URL de ton API — peut être surchargée via NOM_API_URL
|
|
2
|
+
const DEFAULT_API_URL = 'http://localhost:3001/api';
|
|
3
|
+
/**
|
|
4
|
+
* Vérifie le token auprès de l'API et retourne les infos du projet.
|
|
5
|
+
*/
|
|
6
|
+
export async function verifyToken(token, apiUrl = DEFAULT_API_URL) {
|
|
7
|
+
try {
|
|
8
|
+
const res = await fetch(`${apiUrl}/auth/me`, {
|
|
9
|
+
method: 'POST',
|
|
10
|
+
headers: {
|
|
11
|
+
'Content-Type': 'application/json',
|
|
12
|
+
Authorization: `Bearer ${token}`,
|
|
13
|
+
},
|
|
14
|
+
body: JSON.stringify({ token }),
|
|
15
|
+
signal: AbortSignal.timeout(8000), // timeout 8s
|
|
16
|
+
});
|
|
17
|
+
if (!res.ok) {
|
|
18
|
+
if (res.status === 401) {
|
|
19
|
+
return { valid: false, error: 'Token invalide ou expiré.' };
|
|
20
|
+
}
|
|
21
|
+
if (res.status === 404) {
|
|
22
|
+
return { valid: false, error: 'Projet introuvable.' };
|
|
23
|
+
}
|
|
24
|
+
return { valid: false, error: `Erreur serveur (${res.status}).` };
|
|
25
|
+
}
|
|
26
|
+
const data = (await res.json());
|
|
27
|
+
return { valid: true, projectName: data.projectName };
|
|
28
|
+
}
|
|
29
|
+
catch (err) {
|
|
30
|
+
if (err instanceof Error && err.name === 'TimeoutError') {
|
|
31
|
+
return { valid: false, error: 'Timeout — serveur injoignable.' };
|
|
32
|
+
}
|
|
33
|
+
return { valid: false, error: 'Impossible de contacter le serveur.' };
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Construit les headers d'authentification pour les requêtes SDK.
|
|
38
|
+
*/
|
|
39
|
+
export function buildAuthHeaders(config) {
|
|
40
|
+
return {
|
|
41
|
+
Authorization: `Bearer ${config.token}`,
|
|
42
|
+
'X-Nom-Source': 'sdk',
|
|
43
|
+
};
|
|
44
|
+
}
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { intro, outro, text, password, select, confirm, spinner, isCancel, cancel, note, log, } from '@clack/prompts';
|
|
3
|
+
import pc from 'picocolors';
|
|
4
|
+
import { listProjects, createProject, getProjectToken, registerSource } from './api.js';
|
|
5
|
+
import { writeConfig, configExists, normalizeName } from './writer.js';
|
|
6
|
+
import { loginWithBrowser } from './auth-browser.js';
|
|
7
|
+
// ─── Helpers ──────────────────────────────────────────────────────────────────
|
|
8
|
+
function bail(value) {
|
|
9
|
+
if (isCancel(value)) {
|
|
10
|
+
cancel('Setup annulé.');
|
|
11
|
+
process.exit(0);
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
function orionBanner() {
|
|
15
|
+
console.log();
|
|
16
|
+
console.log(pc.cyan(' ╔═══════════════════════╗'));
|
|
17
|
+
console.log(pc.cyan(' ║ ') + pc.bold(pc.white('O R I O N')) + pc.cyan(' · CLI ║'));
|
|
18
|
+
console.log(pc.cyan(' ╚═══════════════════════╝'));
|
|
19
|
+
console.log();
|
|
20
|
+
}
|
|
21
|
+
// ─── Main ─────────────────────────────────────────────────────────────────────
|
|
22
|
+
async function main() {
|
|
23
|
+
orionBanner();
|
|
24
|
+
intro(pc.bgCyan(pc.black(' orion-cli ')));
|
|
25
|
+
// 0. Config existante ?
|
|
26
|
+
if (configExists()) {
|
|
27
|
+
const overwrite = await confirm({
|
|
28
|
+
message: pc.yellow('Un fichier orion.config.ts existe déjà. L\'écraser ?'),
|
|
29
|
+
initialValue: false,
|
|
30
|
+
});
|
|
31
|
+
bail(overwrite);
|
|
32
|
+
if (!overwrite) {
|
|
33
|
+
cancel('Setup annulé — configuration existante conservée.');
|
|
34
|
+
process.exit(0);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
const base = "http://localhost:3001";
|
|
38
|
+
// 2. Connexion au compte Orion
|
|
39
|
+
log.step('Connexion à votre compte Orion');
|
|
40
|
+
// Propose deux méthodes d'auth
|
|
41
|
+
const authMethod = await select({
|
|
42
|
+
message: 'Comment voulez-vous vous connecter ?',
|
|
43
|
+
options: [
|
|
44
|
+
{
|
|
45
|
+
value: 'browser',
|
|
46
|
+
label: '🌐 Via le navigateur',
|
|
47
|
+
hint: 'Recommandé',
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
value: 'token',
|
|
51
|
+
label: '🔑 Token personnel',
|
|
52
|
+
hint: 'Collez votre token depuis le dashboard',
|
|
53
|
+
},
|
|
54
|
+
],
|
|
55
|
+
});
|
|
56
|
+
bail(authMethod);
|
|
57
|
+
let accessToken;
|
|
58
|
+
if (authMethod === 'browser') {
|
|
59
|
+
try {
|
|
60
|
+
accessToken = await loginWithBrowser(base);
|
|
61
|
+
}
|
|
62
|
+
catch (err) {
|
|
63
|
+
cancel(`Échec de l'authentification : ${err instanceof Error ? err.message : String(err)}`);
|
|
64
|
+
process.exit(1);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
else {
|
|
68
|
+
// Fallback : token manuel (utile en CI/CD ou si le navigateur ne s'ouvre pas)
|
|
69
|
+
const manualToken = await password({
|
|
70
|
+
message: 'Collez votre token personnel (depuis orion.dev/settings) :',
|
|
71
|
+
mask: '*',
|
|
72
|
+
});
|
|
73
|
+
bail(manualToken);
|
|
74
|
+
accessToken = manualToken;
|
|
75
|
+
}
|
|
76
|
+
// 3. Choisir ou créer un projet
|
|
77
|
+
const projectsSpinner = spinner();
|
|
78
|
+
projectsSpinner.start('Récupération des projets...');
|
|
79
|
+
let projects = [];
|
|
80
|
+
try {
|
|
81
|
+
projects = await listProjects(base, accessToken);
|
|
82
|
+
projectsSpinner.stop(pc.green(`✓ ${projects.length} projet(s) trouvé(s)`));
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
projectsSpinner.stop(pc.yellow('⚠ Impossible de charger les projets'));
|
|
86
|
+
}
|
|
87
|
+
const projectOptions = [
|
|
88
|
+
...projects.map((p) => ({
|
|
89
|
+
value: p.name,
|
|
90
|
+
label: p.label,
|
|
91
|
+
hint: p.name,
|
|
92
|
+
})),
|
|
93
|
+
{
|
|
94
|
+
value: '__new__',
|
|
95
|
+
label: pc.cyan('+ Créer un nouveau projet'),
|
|
96
|
+
hint: '',
|
|
97
|
+
},
|
|
98
|
+
];
|
|
99
|
+
const selectedProject = await select({
|
|
100
|
+
message: 'Quel projet utiliser ?',
|
|
101
|
+
options: projectOptions,
|
|
102
|
+
});
|
|
103
|
+
bail(selectedProject);
|
|
104
|
+
let projectToken;
|
|
105
|
+
let projectName;
|
|
106
|
+
if (selectedProject === '__new__') {
|
|
107
|
+
// Créer un nouveau projet
|
|
108
|
+
const rawLabel = await text({
|
|
109
|
+
message: 'Nom affiché du projet (label) ?',
|
|
110
|
+
placeholder: 'Mon Backend',
|
|
111
|
+
validate: (v) => !v.trim() ? 'Le label est requis.' : undefined,
|
|
112
|
+
});
|
|
113
|
+
bail(rawLabel);
|
|
114
|
+
const rawName = await text({
|
|
115
|
+
message: 'Identifiant du projet (slug) ?',
|
|
116
|
+
placeholder: normalizeName(rawLabel),
|
|
117
|
+
initialValue: normalizeName(rawLabel),
|
|
118
|
+
validate: (v) => {
|
|
119
|
+
const n = normalizeName(v);
|
|
120
|
+
if (!n)
|
|
121
|
+
return 'Identifiant requis.';
|
|
122
|
+
if (!/^[a-z0-9][a-z0-9-]*$/.test(n))
|
|
123
|
+
return 'Minuscules, chiffres et tirets uniquement.';
|
|
124
|
+
return undefined;
|
|
125
|
+
},
|
|
126
|
+
});
|
|
127
|
+
bail(rawName);
|
|
128
|
+
const createSpinner = spinner();
|
|
129
|
+
createSpinner.start('Création du projet...');
|
|
130
|
+
try {
|
|
131
|
+
const created = await createProject(base, accessToken, normalizeName(rawName), rawLabel.trim());
|
|
132
|
+
projectToken = created.token;
|
|
133
|
+
projectName = created.name;
|
|
134
|
+
createSpinner.stop(pc.green(`✓ Projet "${created.label}" créé`));
|
|
135
|
+
}
|
|
136
|
+
catch (err) {
|
|
137
|
+
createSpinner.stop(pc.red('✗ Échec de la création'));
|
|
138
|
+
cancel(`Erreur : ${err instanceof Error ? err.message : String(err)}`);
|
|
139
|
+
process.exit(1);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
else {
|
|
143
|
+
// Projet existant → récupérer son token
|
|
144
|
+
projectName = selectedProject;
|
|
145
|
+
const tokenSpinner = spinner();
|
|
146
|
+
tokenSpinner.start('Récupération du token...');
|
|
147
|
+
try {
|
|
148
|
+
projectToken = await getProjectToken(base, accessToken, projectName);
|
|
149
|
+
tokenSpinner.stop(pc.green('✓ Token récupéré'));
|
|
150
|
+
}
|
|
151
|
+
catch (err) {
|
|
152
|
+
tokenSpinner.stop(pc.red('✗ Impossible de récupérer le token'));
|
|
153
|
+
cancel(`Erreur : ${err instanceof Error ? err.message : String(err)}`);
|
|
154
|
+
process.exit(1);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
// 4. Nom de la source
|
|
158
|
+
const source = await text({
|
|
159
|
+
message: 'Nom de la source (identifie l\'origine des logs) ?',
|
|
160
|
+
placeholder: 'api-backend',
|
|
161
|
+
validate: (v) => {
|
|
162
|
+
if (!v.trim())
|
|
163
|
+
return 'Requis.';
|
|
164
|
+
if (!/^[a-z0-9_-]+$/.test(v))
|
|
165
|
+
return 'Minuscules, chiffres, et - uniquement.';
|
|
166
|
+
return undefined;
|
|
167
|
+
},
|
|
168
|
+
});
|
|
169
|
+
bail(source);
|
|
170
|
+
// 5. Description de la source
|
|
171
|
+
const description = await text({
|
|
172
|
+
message: 'Description de la source ?',
|
|
173
|
+
placeholder: 'Gestion des utilisateurs',
|
|
174
|
+
});
|
|
175
|
+
bail(description);
|
|
176
|
+
// 6. Environnement
|
|
177
|
+
const environment = await select({
|
|
178
|
+
message: 'Environnement ?',
|
|
179
|
+
options: [
|
|
180
|
+
{ value: 'prod', label: 'Production', hint: 'prod' },
|
|
181
|
+
{ value: 'dev', label: 'Development', hint: 'dev' },
|
|
182
|
+
{ value: 'staging', label: 'Staging', hint: 'staging' },
|
|
183
|
+
{ value: 'test', label: 'Test', hint: 'test' },
|
|
184
|
+
],
|
|
185
|
+
});
|
|
186
|
+
bail(environment);
|
|
187
|
+
// 7. Register source on server
|
|
188
|
+
let result = await registerSource(base, accessToken, projectName, source, description, environment);
|
|
189
|
+
// 7. Écriture
|
|
190
|
+
const writeSpinner = spinner();
|
|
191
|
+
writeSpinner.start('Écriture de orion.config.ts...');
|
|
192
|
+
const { configPath } = writeConfig({
|
|
193
|
+
token: projectToken,
|
|
194
|
+
projectName: projectName,
|
|
195
|
+
sourceName: result.name,
|
|
196
|
+
}, process.cwd());
|
|
197
|
+
writeSpinner.stop(pc.green('✓ Configuration écrite'));
|
|
198
|
+
// 8. Résumé
|
|
199
|
+
note([
|
|
200
|
+
`Projet : ${pc.bold(projectName)}`,
|
|
201
|
+
`Source : ${pc.bold(result.name)}`,
|
|
202
|
+
`Config : ${pc.cyan(configPath)}`,
|
|
203
|
+
].join('\n'), 'Récapitulatif');
|
|
204
|
+
// 9. Outro
|
|
205
|
+
outro(pc.green('✓ Setup terminé !\n\n') +
|
|
206
|
+
' Installez le SDK :\n' +
|
|
207
|
+
pc.cyan(' npm install orion\n\n') +
|
|
208
|
+
' Puis dans votre code :\n' +
|
|
209
|
+
pc.gray(" import { createLogger } from 'orion'\n") +
|
|
210
|
+
pc.gray(' const logger = await createLogger()\n') +
|
|
211
|
+
pc.gray(" logger.info('Hello from Orion!')"));
|
|
212
|
+
}
|
|
213
|
+
main().catch((err) => {
|
|
214
|
+
console.error(pc.red('\nErreur inattendue :'), err);
|
|
215
|
+
process.exit(1);
|
|
216
|
+
});
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
export type Environment = 'production' | 'development' | 'staging' | 'test';
|
|
2
|
+
export interface LoginResponse {
|
|
3
|
+
accessToken: string;
|
|
4
|
+
user: {
|
|
5
|
+
id: number;
|
|
6
|
+
email: string;
|
|
7
|
+
pseudo: string;
|
|
8
|
+
first_name: string;
|
|
9
|
+
last_name: string;
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
export interface Project {
|
|
13
|
+
id: string;
|
|
14
|
+
name: string;
|
|
15
|
+
label: string;
|
|
16
|
+
}
|
|
17
|
+
export interface CreateProjectResponse {
|
|
18
|
+
id: number;
|
|
19
|
+
name: string;
|
|
20
|
+
label: string;
|
|
21
|
+
token: string;
|
|
22
|
+
}
|
|
23
|
+
export interface GetTokenResponse {
|
|
24
|
+
token: string;
|
|
25
|
+
}
|
|
26
|
+
export interface OrionConfig {
|
|
27
|
+
token: string;
|
|
28
|
+
projectName: string;
|
|
29
|
+
sourceName: string;
|
|
30
|
+
}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/writer.d.ts
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { OrionConfig } from './types.js';
|
|
2
|
+
export declare function writeConfig(config: OrionConfig, targetDir?: string): {
|
|
3
|
+
configPath: string;
|
|
4
|
+
};
|
|
5
|
+
export declare function configExists(targetDir?: string): boolean;
|
|
6
|
+
export declare function normalizeName(raw: string): string;
|
package/dist/writer.js
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
export function writeConfig(config, targetDir = process.cwd()) {
|
|
4
|
+
const configPath = path.join(targetDir, 'orion.config.ts');
|
|
5
|
+
const content = [
|
|
6
|
+
`import { defineConfig } from 'orion-cli'`,
|
|
7
|
+
``,
|
|
8
|
+
`export default defineConfig({`,
|
|
9
|
+
` token: '${config.token}',`,
|
|
10
|
+
` projectName: '${config.projectName}',`,
|
|
11
|
+
` sourceName: '${config.sourceName}',`,
|
|
12
|
+
`})`,
|
|
13
|
+
``,
|
|
14
|
+
].join('\n');
|
|
15
|
+
fs.writeFileSync(configPath, content, 'utf-8');
|
|
16
|
+
return { configPath };
|
|
17
|
+
}
|
|
18
|
+
export function configExists(targetDir = process.cwd()) {
|
|
19
|
+
return fs.existsSync(path.join(targetDir, 'orion.config.ts'));
|
|
20
|
+
}
|
|
21
|
+
// Normalise un nom de projet comme le fait ton backend
|
|
22
|
+
export function normalizeName(raw) {
|
|
23
|
+
return raw
|
|
24
|
+
.toLowerCase()
|
|
25
|
+
.trim()
|
|
26
|
+
.replace(/\s+/g, '-')
|
|
27
|
+
.replace(/-+/g, '-')
|
|
28
|
+
.replace(/^-|-$/g, '');
|
|
29
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@orion-monitoring/cli",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "CLI to initialize an Orion logging project",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"orion-cli": "./dist/index.js",
|
|
8
|
+
"create-orion": "./dist/index.js"
|
|
9
|
+
},
|
|
10
|
+
"scripts": {
|
|
11
|
+
"build": "tsc",
|
|
12
|
+
"dev": "tsx src/index.ts",
|
|
13
|
+
"start": "node dist/index.js"
|
|
14
|
+
},
|
|
15
|
+
"dependencies": {
|
|
16
|
+
"@clack/prompts": "^0.7.0",
|
|
17
|
+
"open": "^11.0.0",
|
|
18
|
+
"picocolors": "^1.0.1"
|
|
19
|
+
},
|
|
20
|
+
"devDependencies": {
|
|
21
|
+
"@types/node": "^20.0.0",
|
|
22
|
+
"tsx": "^4.0.0",
|
|
23
|
+
"typescript": "^5.0.0"
|
|
24
|
+
},
|
|
25
|
+
"engines": {
|
|
26
|
+
"node": ">=18.0.0"
|
|
27
|
+
},
|
|
28
|
+
"files": [
|
|
29
|
+
"dist",
|
|
30
|
+
"README.md"
|
|
31
|
+
]
|
|
32
|
+
}
|