pokt-cli 1.0.3 → 1.0.5
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/bin/pokt.js +7 -0
- package/dist/chat/loop.js +159 -8
- package/dist/commands/chat.js +1 -1
- package/dist/commands/pro.d.ts +6 -0
- package/dist/commands/pro.js +37 -0
- package/dist/config.d.ts +2 -0
- package/dist/config.js +2 -0
- package/dist/mcp/client.js +1 -1
- package/dist/ui.js +1 -1
- package/dist/util/openBrowser.d.ts +2 -0
- package/dist/util/openBrowser.js +13 -0
- package/package.json +1 -1
package/dist/bin/pokt.js
CHANGED
|
@@ -8,6 +8,7 @@ import { providerCommand } from '../commands/provider.js';
|
|
|
8
8
|
import { mcpCommand } from '../commands/mcp.js';
|
|
9
9
|
import { updateCommand } from '../commands/update.js';
|
|
10
10
|
import { uninstallCommand } from '../commands/uninstall.js';
|
|
11
|
+
import { proCommand, runProFlow } from '../commands/pro.js';
|
|
11
12
|
import prompts from 'prompts';
|
|
12
13
|
import chalk from 'chalk';
|
|
13
14
|
import { ui } from '../ui.js';
|
|
@@ -26,6 +27,7 @@ else {
|
|
|
26
27
|
.command(mcpCommand)
|
|
27
28
|
.command(updateCommand)
|
|
28
29
|
.command(uninstallCommand)
|
|
30
|
+
.command(proCommand)
|
|
29
31
|
.demandCommand(1, 'You need at least one command before moving on')
|
|
30
32
|
.help()
|
|
31
33
|
.parse();
|
|
@@ -51,6 +53,7 @@ async function showMenu() {
|
|
|
51
53
|
{ title: '🏠 Switch API Provider (casa de API)', value: 'provider' },
|
|
52
54
|
{ title: '🔌 MCP Servers (tools externos)', value: 'mcp' },
|
|
53
55
|
{ title: '⚙️ Configure API Keys / Tokens', value: 'config' },
|
|
56
|
+
{ title: '⭐ Torne-se Pro (site — pagamento + chave)', value: 'pro' },
|
|
54
57
|
{ title: '🔄 Atualizar Pokt CLI', value: 'update' },
|
|
55
58
|
{ title: '🗑️ Remover Pokt CLI', value: 'uninstall' },
|
|
56
59
|
{ title: '❌ Exit', value: 'exit' }
|
|
@@ -75,6 +78,10 @@ async function showMenu() {
|
|
|
75
78
|
else if (response.action === 'config') {
|
|
76
79
|
await handleConfigMenu();
|
|
77
80
|
}
|
|
81
|
+
else if (response.action === 'pro') {
|
|
82
|
+
runProFlow();
|
|
83
|
+
return showMenu();
|
|
84
|
+
}
|
|
78
85
|
else if (response.action === 'update') {
|
|
79
86
|
const { updateCommand } = await import('../commands/update.js');
|
|
80
87
|
await updateCommand.handler({});
|
package/dist/chat/loop.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import prompts from 'prompts';
|
|
2
2
|
import ora from 'ora';
|
|
3
3
|
import { ui } from '../ui.js';
|
|
4
|
+
import { runProFlow } from '../commands/pro.js';
|
|
4
5
|
import { config } from '../config.js';
|
|
5
6
|
import { getClient } from './client.js';
|
|
6
7
|
import { tools, executeTool } from './tools.js';
|
|
@@ -13,12 +14,17 @@ CORE CAPABILITIES:
|
|
|
13
14
|
2. **Autonomous Coding**: You can create new files, rewrite existing ones, and run terminal commands.
|
|
14
15
|
3. **Problem Solving**: You analyze errors and propose/apply fixes.
|
|
15
16
|
|
|
17
|
+
CRITICAL - FILE CREATION/EDITS (this API does NOT support tool calls):
|
|
18
|
+
- Do NOT reply with only "We will call read_file", "We will call write_file" or similar. Those tools will NOT run. The user will get no file.
|
|
19
|
+
- You MUST output the complete file content in a markdown code block so the CLI can create/edit the file. Format: mention the filename (e.g. hello.py or **hello.py**) then a newline then \`\`\`python then newline then the full file content then \`\`\`.
|
|
20
|
+
- For edits: first "read" the file by inferring its content from the user request and project context, then output the full updated file in a \`\`\`python (or correct language) block with the filename mentioned just above the block.
|
|
21
|
+
- Never end your response with only an intention to call a tool. Always include the actual code in a block.
|
|
22
|
+
|
|
16
23
|
GUIDELINES:
|
|
17
24
|
- You will receive the user request first, then the current project structure. Use the project structure to understand the context before creating or editing anything.
|
|
18
25
|
- When asked to fix something, first **read** the relevant files to understand the context.
|
|
19
|
-
- When creating a project, start by planning the structure, then use \`write_file\` to create
|
|
20
|
-
-
|
|
21
|
-
- You have full access to the current terminal. You can run \`npm install\`, \`tsc\`, or any other command.
|
|
26
|
+
- When creating a project, start by planning the structure, then use \`write_file\` to create each file.
|
|
27
|
+
- You have full access to the current terminal. You can run \`run_command\` for \`npm install\`, \`tsc\`, or any other command.
|
|
22
28
|
- Be extremely concise in your explanations.
|
|
23
29
|
- The current working directory is: ${process.cwd()}
|
|
24
30
|
`;
|
|
@@ -58,7 +64,9 @@ export async function startChatLoop(modelConfig) {
|
|
|
58
64
|
];
|
|
59
65
|
while (true) {
|
|
60
66
|
console.log('');
|
|
61
|
-
|
|
67
|
+
const cwd = process.cwd();
|
|
68
|
+
console.log(ui.dim(`Diretório atual: ${cwd}`));
|
|
69
|
+
console.log(ui.shortcutsLine('shift+tab to accept edits', '? · /pro (Torne-se Pro)'));
|
|
62
70
|
const response = await prompts({
|
|
63
71
|
type: 'text',
|
|
64
72
|
name: 'input',
|
|
@@ -72,6 +80,20 @@ export async function startChatLoop(modelConfig) {
|
|
|
72
80
|
console.log(ui.dim('Goodbye!'));
|
|
73
81
|
break;
|
|
74
82
|
}
|
|
83
|
+
const trimmed = userInput.trim();
|
|
84
|
+
const low = trimmed.toLowerCase();
|
|
85
|
+
if (low === '/pro' || low === '/torne-se-pro' || low === 'torne-se pro') {
|
|
86
|
+
runProFlow();
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
if (trimmed === '?') {
|
|
90
|
+
console.log(ui.dim(`
|
|
91
|
+
Atalhos:
|
|
92
|
+
${ui.accent('/pro')} ou ${ui.accent('/torne-se-pro')} — abrir Pokt Pro no navegador (pagamento + chave)
|
|
93
|
+
exit, ${ui.accent('/quit')} — sair do chat
|
|
94
|
+
`));
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
75
97
|
messages.push({ role: 'user', content: userInput });
|
|
76
98
|
// Primeiro o modelo vê o pedido; depois carregamos a estrutura do projeto para ele entender e então criar/editar
|
|
77
99
|
const isFirstUserMessage = messages.filter(m => m.role === 'user').length === 1;
|
|
@@ -87,6 +109,83 @@ export async function startChatLoop(modelConfig) {
|
|
|
87
109
|
}
|
|
88
110
|
const MAX_429_RETRIES = 3;
|
|
89
111
|
const BASE_429_DELAY_MS = 5000;
|
|
112
|
+
/** Extensões que consideramos como arquivos de código para aplicar fallback */
|
|
113
|
+
const CODE_EXT = /\.(py|js|ts|tsx|jsx|html|css|json|md|txt|java|go|rs|c|cpp|rb|php)$/i;
|
|
114
|
+
/**
|
|
115
|
+
* Quando a API não retorna tool_calls, alguns backends só devolvem texto.
|
|
116
|
+
* Extrai blocos de código da resposta (```lang\n...\n```) e, se encontrar
|
|
117
|
+
* um nome de arquivo mencionado antes do bloco, aplica write_file.
|
|
118
|
+
*/
|
|
119
|
+
/** Blocos de comando "como rodar" (bash/sh de 1–2 linhas) não viram arquivo para não poluir. */
|
|
120
|
+
function isRunCommandOnly(lang, code) {
|
|
121
|
+
const shellLike = /^(bash|sh|shell|zsh)$/i.test(lang);
|
|
122
|
+
const lines = code.split('\n').filter((l) => l.trim().length > 0);
|
|
123
|
+
return shellLike && lines.length <= 2;
|
|
124
|
+
}
|
|
125
|
+
/** Remove markdown/formatting do nome de arquivo (ex: **hello.py** → hello.py). */
|
|
126
|
+
function cleanFilename(candidate) {
|
|
127
|
+
return candidate.replace(/^[\s*`'"]+/g, '').replace(/[\s*`'")\]\s]+$/g, '').trim();
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Aplica blocos de código da resposta e retorna conteúdo para exibição (sem repetir o código).
|
|
131
|
+
* - Detecta nome de arquivo na mensagem (ex: "updated **hello.py**") para editar o existente.
|
|
132
|
+
* - Blocos já aplicados são substituídos por "[Código aplicado ao arquivo: path]" na mensagem exibida.
|
|
133
|
+
*/
|
|
134
|
+
/** Garante que o conteúdo da mensagem seja string (algumas APIs devolvem array). */
|
|
135
|
+
function messageContentToString(content) {
|
|
136
|
+
if (typeof content === 'string')
|
|
137
|
+
return content;
|
|
138
|
+
if (Array.isArray(content)) {
|
|
139
|
+
return content
|
|
140
|
+
.map((part) => (typeof part === 'object' && part != null && 'text' in part ? part.text : String(part)))
|
|
141
|
+
.join('');
|
|
142
|
+
}
|
|
143
|
+
return content != null ? String(content) : '';
|
|
144
|
+
}
|
|
145
|
+
async function applyCodeBlocksFromContent(content) {
|
|
146
|
+
const codeBlockRe = /```(\w*)\n([\s\S]*?)```/g;
|
|
147
|
+
const appliedBlocks = [];
|
|
148
|
+
let applied = false;
|
|
149
|
+
let m;
|
|
150
|
+
const matches = [];
|
|
151
|
+
while ((m = codeBlockRe.exec(content)) !== null) {
|
|
152
|
+
matches.push({
|
|
153
|
+
fullMatch: m[0],
|
|
154
|
+
index: m.index,
|
|
155
|
+
lang: m[1] || '',
|
|
156
|
+
code: m[2].replace(/\r\n/g, '\n').trimEnd(),
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
for (const { fullMatch, index, lang, code } of matches) {
|
|
160
|
+
if (isRunCommandOnly(lang, code))
|
|
161
|
+
continue;
|
|
162
|
+
const beforeBlock = content.substring(0, index);
|
|
163
|
+
// Nome de arquivo: aceita "**hello.py**", "hello.py" antes de espaço/newline/backtick, etc.
|
|
164
|
+
const fileMatch = beforeBlock.match(/(\S+\.(?:py|js|ts|tsx|jsx|html|css|json|md|txt|java|go|rs|c|cpp|rb|php))(?=\s|$|[:.)\]*`"])/gi);
|
|
165
|
+
const rawCandidate = fileMatch ? fileMatch[fileMatch.length - 1].trim() : null;
|
|
166
|
+
const candidate = rawCandidate ? cleanFilename(rawCandidate) : null;
|
|
167
|
+
const path = candidate && CODE_EXT.test(candidate) ? candidate : (lang === 'python' ? 'generated.py' : lang ? `generated.${lang}` : null);
|
|
168
|
+
if (path && code) {
|
|
169
|
+
try {
|
|
170
|
+
console.log(ui.warn(`\n[Fallback] Aplicando código da resposta ao arquivo: ${path}`));
|
|
171
|
+
await executeTool('write_file', JSON.stringify({ path, content: code }));
|
|
172
|
+
applied = true;
|
|
173
|
+
appliedBlocks.push({ start: index, end: index + fullMatch.length, path });
|
|
174
|
+
}
|
|
175
|
+
catch {
|
|
176
|
+
// ignora falha em um bloco
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
// Monta mensagem para exibição: blocos aplicados viram uma linha curta (evita repetir o código)
|
|
181
|
+
let displayContent = content;
|
|
182
|
+
for (let i = appliedBlocks.length - 1; i >= 0; i--) {
|
|
183
|
+
const { start, end, path } = appliedBlocks[i];
|
|
184
|
+
const placeholder = `\n${ui.dim('[Código aplicado ao arquivo: ' + path + ']')}\n`;
|
|
185
|
+
displayContent = displayContent.substring(0, start) + placeholder + displayContent.substring(end);
|
|
186
|
+
}
|
|
187
|
+
return { applied, displayContent };
|
|
188
|
+
}
|
|
90
189
|
async function createCompletionWithRetry(client, modelId, messages, toolsList) {
|
|
91
190
|
let lastError;
|
|
92
191
|
for (let attempt = 0; attempt <= MAX_429_RETRIES; attempt++) {
|
|
@@ -117,10 +216,13 @@ async function processLLMResponse(client, modelId, messages, toolsList) {
|
|
|
117
216
|
let completion = await createCompletionWithRetry(client, modelId, messages, toolsList);
|
|
118
217
|
let message = completion.choices[0].message;
|
|
119
218
|
spinner.stop();
|
|
219
|
+
let writeFileExecutedThisTurn = false;
|
|
120
220
|
while (message.tool_calls && message.tool_calls.length > 0) {
|
|
121
221
|
messages.push(message);
|
|
122
222
|
for (const toolCall of message.tool_calls) {
|
|
123
223
|
const name = toolCall.function.name;
|
|
224
|
+
if (name === 'write_file')
|
|
225
|
+
writeFileExecutedThisTurn = true;
|
|
124
226
|
const args = toolCall.function.arguments ?? '{}';
|
|
125
227
|
console.log(ui.warn(`\n[Executing Tool: ${name}]`));
|
|
126
228
|
console.log(ui.dim(`Arguments: ${args}`));
|
|
@@ -141,10 +243,59 @@ async function processLLMResponse(client, modelId, messages, toolsList) {
|
|
|
141
243
|
message = completion.choices[0].message;
|
|
142
244
|
spinner.stop();
|
|
143
245
|
}
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
246
|
+
const rawContent = message.content;
|
|
247
|
+
let contentStr = messageContentToString(rawContent);
|
|
248
|
+
let finalContent = rawContent ?? contentStr;
|
|
249
|
+
// Quando a API não executa tools, tentar aplicar blocos de código da resposta
|
|
250
|
+
if (!writeFileExecutedThisTurn) {
|
|
251
|
+
let result = await applyCodeBlocksFromContent(contentStr);
|
|
252
|
+
// Se a IA só disse "We will call read_file/write_file" e não há código, pedir o código em um follow-up
|
|
253
|
+
const looksLikeToolIntentOnly = /(We will call|We need to call|Let's call|I will call)\s+(read_file|write_file|run_command)/i.test(contentStr)
|
|
254
|
+
|| (/call\s+(read_file|write_file)/i.test(contentStr) && contentStr.length < 400);
|
|
255
|
+
if (!result.applied && looksLikeToolIntentOnly) {
|
|
256
|
+
messages.push({ role: 'assistant', content: rawContent ?? contentStr });
|
|
257
|
+
const followUpSystem = `This API does not support tool calls. You must NOT reply with "We will call X". Output the complete file content in a markdown code block so the user's CLI can create/edit the file. Format: mention the filename (e.g. hello.py) then newline then \`\`\`python then newline then the FULL file content then \`\`\`. Do that now for the user's last request.`;
|
|
258
|
+
messages.push({ role: 'system', content: followUpSystem });
|
|
259
|
+
spinner.start('Getting code...');
|
|
260
|
+
const followUp = await createCompletionWithRetry(client, modelId, messages, toolsList);
|
|
261
|
+
spinner.stop();
|
|
262
|
+
const followUpMsg = followUp.choices[0].message;
|
|
263
|
+
const followUpStr = messageContentToString(followUpMsg.content);
|
|
264
|
+
if (followUpStr.trim() !== '') {
|
|
265
|
+
result = await applyCodeBlocksFromContent(followUpStr);
|
|
266
|
+
contentStr = followUpStr;
|
|
267
|
+
finalContent = followUpMsg.content ?? followUpStr;
|
|
268
|
+
messages.push({ role: 'assistant', content: finalContent });
|
|
269
|
+
}
|
|
270
|
+
else {
|
|
271
|
+
messages.push({ role: 'assistant', content: '' });
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
if (contentStr.trim() !== '') {
|
|
275
|
+
let contentToShow = result.applied ? result.displayContent : contentStr;
|
|
276
|
+
console.log('\n' + ui.labelPokt());
|
|
277
|
+
console.log(contentToShow);
|
|
278
|
+
if (!messages.some((m) => m.role === 'assistant' && m.content === finalContent)) {
|
|
279
|
+
messages.push({ role: 'assistant', content: finalContent });
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
else {
|
|
283
|
+
console.log('\n' + ui.labelPokt());
|
|
284
|
+
console.log(ui.dim('(A IA não retornou código utilizável. Tente reformular o pedido.)'));
|
|
285
|
+
messages.push({ role: 'assistant', content: '' });
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
else {
|
|
289
|
+
if (contentStr.trim() !== '') {
|
|
290
|
+
console.log('\n' + ui.labelPokt());
|
|
291
|
+
console.log(contentStr);
|
|
292
|
+
messages.push({ role: 'assistant', content: finalContent });
|
|
293
|
+
}
|
|
294
|
+
else {
|
|
295
|
+
console.log('\n' + ui.labelPokt());
|
|
296
|
+
console.log(ui.dim('(Sem resposta de texto.)'));
|
|
297
|
+
messages.push({ role: 'assistant', content: '' });
|
|
298
|
+
}
|
|
148
299
|
}
|
|
149
300
|
}
|
|
150
301
|
catch (error) {
|
package/dist/commands/chat.js
CHANGED
|
@@ -34,7 +34,7 @@ export const chatCommand = {
|
|
|
34
34
|
console.log('');
|
|
35
35
|
}
|
|
36
36
|
console.log(ui.dim('Type "exit" or /quit to end the session.'));
|
|
37
|
-
console.log(ui.statusBar({ model: `/model ${activeModel.provider} (${activeModel.id})` }));
|
|
37
|
+
console.log(ui.statusBar({ cwd: process.cwd(), model: `/model ${activeModel.provider} (${activeModel.id})` }));
|
|
38
38
|
console.log('');
|
|
39
39
|
await startChatLoop(activeModel);
|
|
40
40
|
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { getProPurchaseUrl } from '../config.js';
|
|
2
|
+
import { openBrowser } from '../util/openBrowser.js';
|
|
3
|
+
import { ui } from '../ui.js';
|
|
4
|
+
export const proCommand = {
|
|
5
|
+
command: 'pro',
|
|
6
|
+
aliases: ['Pro'],
|
|
7
|
+
describe: 'Abre a página inicial do Controller (botão "Torne-se Pro"). Use --url só para imprimir o link.',
|
|
8
|
+
builder: (yargs) => yargs.option('url', {
|
|
9
|
+
type: 'boolean',
|
|
10
|
+
default: false,
|
|
11
|
+
describe: 'Só mostra a URL no terminal (não abre o navegador)',
|
|
12
|
+
}),
|
|
13
|
+
handler: (argv) => {
|
|
14
|
+
if (argv.url) {
|
|
15
|
+
console.log(getProPurchaseUrl());
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
runProFlow();
|
|
19
|
+
},
|
|
20
|
+
};
|
|
21
|
+
/** Usado pelo menu principal e pelo chat (/pro). */
|
|
22
|
+
export function runProFlow(printOnlyUrl = false) {
|
|
23
|
+
const proHomeUrl = getProPurchaseUrl();
|
|
24
|
+
if (printOnlyUrl) {
|
|
25
|
+
console.log(proHomeUrl);
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
console.log(ui.dim('Pokt Pro — abra o site e clique em "Torne-se Pro" (pagamento + chave imediata).\n'));
|
|
29
|
+
console.log(ui.accent(proHomeUrl));
|
|
30
|
+
try {
|
|
31
|
+
openBrowser(proHomeUrl);
|
|
32
|
+
console.log(ui.success('\nAbrindo no navegador… Se não abrir, copie o link acima.\n'));
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
console.log(ui.warn('Não foi possível abrir o navegador. Copie o link acima.\n'));
|
|
36
|
+
}
|
|
37
|
+
}
|
package/dist/config.d.ts
CHANGED
|
@@ -30,6 +30,8 @@ interface AppConfig {
|
|
|
30
30
|
}
|
|
31
31
|
export declare const config: Conf<AppConfig>;
|
|
32
32
|
export declare const getControllerBaseUrl: () => string;
|
|
33
|
+
/** Página inicial do Pokt Pro (aí tem o botão de assinatura/pagamento). */
|
|
34
|
+
export declare const getProPurchaseUrl: () => string;
|
|
33
35
|
/** Prioridade: modelo ativo explícito → Pokt (controller) se token setado → OpenRouter → Gemini → Ollama Cloud → Ollama local */
|
|
34
36
|
export declare function getEffectiveActiveModel(): ModelConfig | null;
|
|
35
37
|
export {};
|
package/dist/config.js
CHANGED
|
@@ -33,6 +33,8 @@ export const getControllerBaseUrl = () => {
|
|
|
33
33
|
const url = config.get('controllerBaseUrl') || DEFAULT_CONTROLLER_URL;
|
|
34
34
|
return url.replace(/\/$/, '');
|
|
35
35
|
};
|
|
36
|
+
/** Página inicial do Pokt Pro (aí tem o botão de assinatura/pagamento). */
|
|
37
|
+
export const getProPurchaseUrl = () => getControllerBaseUrl();
|
|
36
38
|
/** Prioridade: modelo ativo explícito → Pokt (controller) se token setado → OpenRouter → Gemini → Ollama Cloud → Ollama local */
|
|
37
39
|
export function getEffectiveActiveModel() {
|
|
38
40
|
const explicit = config.get('activeModel');
|
package/dist/mcp/client.js
CHANGED
|
@@ -39,7 +39,7 @@ export async function connectMcpServer(serverConfig) {
|
|
|
39
39
|
args: serverConfig.args ?? [],
|
|
40
40
|
env: process.env,
|
|
41
41
|
});
|
|
42
|
-
const client = new Client({ name: 'pokt-cli', version: '1.0.
|
|
42
|
+
const client = new Client({ name: 'pokt-cli', version: '1.0.5' });
|
|
43
43
|
await client.connect(transport);
|
|
44
44
|
const list = await client.listTools();
|
|
45
45
|
const tools = (list.tools ?? []).map((t) => ({
|
package/dist/ui.js
CHANGED
|
@@ -2,7 +2,7 @@ import chalk from 'chalk';
|
|
|
2
2
|
import fs from 'fs';
|
|
3
3
|
import path from 'path';
|
|
4
4
|
import { execSync } from 'child_process';
|
|
5
|
-
const VERSION = '1.0.
|
|
5
|
+
const VERSION = '1.0.5';
|
|
6
6
|
/** Logo em estilo chevron com gradiente (azul → rosa → roxo) */
|
|
7
7
|
function logo() {
|
|
8
8
|
const c = (s, color) => color(s);
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { spawn } from 'child_process';
|
|
2
|
+
/** Abre a URL no navegador padrão (Windows / macOS / Linux). */
|
|
3
|
+
export function openBrowser(url) {
|
|
4
|
+
if (process.platform === 'win32') {
|
|
5
|
+
spawn('cmd', ['/c', 'start', '""', url], { detached: true, stdio: 'ignore' }).unref();
|
|
6
|
+
}
|
|
7
|
+
else if (process.platform === 'darwin') {
|
|
8
|
+
spawn('open', [url], { detached: true, stdio: 'ignore' }).unref();
|
|
9
|
+
}
|
|
10
|
+
else {
|
|
11
|
+
spawn('xdg-open', [url], { detached: true, stdio: 'ignore' }).unref();
|
|
12
|
+
}
|
|
13
|
+
}
|