@promptcellar/pc 0.2.0 → 0.3.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/bin/pc.js CHANGED
@@ -13,11 +13,12 @@ import { update } from '../src/commands/update.js';
13
13
  program
14
14
  .name('pc')
15
15
  .description('PromptCellar CLI - sync prompts between your terminal and the cloud')
16
- .version('0.1.0');
16
+ .version('0.3.0');
17
17
 
18
18
  program
19
19
  .command('login')
20
- .description('Authenticate with PromptCellar')
20
+ .description('Authenticate with PromptCellar via browser')
21
+ .option('-u, --url <url>', 'API URL (for self-hosted instances)')
21
22
  .action(login);
22
23
 
23
24
  program
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@promptcellar/pc",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "CLI for PromptCellar - sync prompts between your terminal and the cloud",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
@@ -40,6 +40,7 @@
40
40
  "ora": "^8.0.0",
41
41
  "inquirer": "^9.2.0",
42
42
  "socket.io-client": "^4.6.0",
43
- "node-fetch": "^3.3.0"
43
+ "node-fetch": "^3.3.0",
44
+ "open": "^10.0.0"
44
45
  }
45
46
  }
@@ -1,56 +1,127 @@
1
- import inquirer from 'inquirer';
2
1
  import ora from 'ora';
3
2
  import chalk from 'chalk';
4
- import { setApiKey, setApiUrl, isLoggedIn } from '../lib/config.js';
5
- import { testConnection } from '../lib/api.js';
3
+ import open from 'open';
4
+ import { setApiKey, setApiUrl, isLoggedIn, getApiUrl } from '../lib/config.js';
6
5
 
7
- export async function login() {
8
- if (isLoggedIn()) {
9
- const { overwrite } = await inquirer.prompt([{
10
- type: 'confirm',
11
- name: 'overwrite',
12
- message: 'Already logged in. Replace existing credentials?',
13
- default: false
14
- }]);
15
-
16
- if (!overwrite) {
17
- console.log('Login cancelled.');
18
- return;
19
- }
6
+ const DEFAULT_API_URL = 'https://prompts.weldedanvil.com';
7
+
8
+ async function initiateDeviceAuth(apiUrl) {
9
+ const response = await fetch(`${apiUrl}/auth/device`, {
10
+ method: 'POST',
11
+ headers: { 'Content-Type': 'application/json' },
12
+ body: JSON.stringify({ device_name: getDeviceName() })
13
+ });
14
+
15
+ if (!response.ok) {
16
+ throw new Error('Failed to initiate device authentication');
20
17
  }
21
18
 
22
- const answers = await inquirer.prompt([
23
- {
24
- type: 'input',
25
- name: 'apiKey',
26
- message: 'Enter your PromptCellar API key:',
27
- validate: (input) => {
28
- if (!input.trim()) return 'API key is required';
29
- if (!input.startsWith('pk_')) return 'Invalid API key format (should start with pk_)';
30
- return true;
31
- }
32
- },
33
- {
34
- type: 'input',
35
- name: 'apiUrl',
36
- message: 'API URL (press enter for default):',
37
- default: 'https://prompts.weldedanvil.com'
19
+ return response.json();
20
+ }
21
+
22
+ async function pollForApproval(apiUrl, deviceCode, interval, expiresIn) {
23
+ const startTime = Date.now();
24
+ const expiresAt = startTime + (expiresIn * 1000);
25
+
26
+ while (Date.now() < expiresAt) {
27
+ await sleep(interval * 1000);
28
+
29
+ const response = await fetch(`${apiUrl}/auth/device/poll`, {
30
+ method: 'POST',
31
+ headers: { 'Content-Type': 'application/json' },
32
+ body: JSON.stringify({ device_code: deviceCode })
33
+ });
34
+
35
+ const data = await response.json();
36
+
37
+ if (response.ok && data.access_token) {
38
+ return data;
39
+ }
40
+
41
+ if (data.error === 'authorization_pending') {
42
+ continue;
43
+ }
44
+
45
+ if (data.error === 'expired_token') {
46
+ throw new Error('Authorization request expired. Please try again.');
38
47
  }
39
- ]);
40
48
 
41
- const spinner = ora('Testing connection...').start();
49
+ if (data.error === 'access_denied') {
50
+ throw new Error('Authorization denied.');
51
+ }
52
+ }
53
+
54
+ throw new Error('Authorization request timed out. Please try again.');
55
+ }
42
56
 
43
- setApiKey(answers.apiKey.trim());
44
- setApiUrl(answers.apiUrl.trim());
57
+ function getDeviceName() {
58
+ const os = process.platform;
59
+ const hostname = process.env.HOSTNAME || process.env.COMPUTERNAME || 'Unknown';
45
60
 
46
- const result = await testConnection();
61
+ const osNames = {
62
+ darwin: 'macOS',
63
+ linux: 'Linux',
64
+ win32: 'Windows'
65
+ };
66
+
67
+ return `${osNames[os] || os} - ${hostname}`;
68
+ }
69
+
70
+ function sleep(ms) {
71
+ return new Promise(resolve => setTimeout(resolve, ms));
72
+ }
73
+
74
+ export async function login(options) {
75
+ const apiUrl = options?.url || DEFAULT_API_URL;
76
+
77
+ if (isLoggedIn()) {
78
+ console.log(chalk.yellow('Already logged in.'));
79
+ console.log('Run ' + chalk.cyan('pc logout') + ' first to switch accounts.\n');
80
+ return;
81
+ }
82
+
83
+ console.log(chalk.bold('\nPromptCellar CLI Login\n'));
84
+
85
+ const spinner = ora('Initiating login...').start();
86
+
87
+ try {
88
+ // Step 1: Request device code
89
+ const authData = await initiateDeviceAuth(apiUrl);
90
+ spinner.stop();
91
+
92
+ // Step 2: Show code and open browser
93
+ console.log(chalk.bold('Your verification code:\n'));
94
+ console.log(chalk.cyan.bold(` ${authData.user_code}\n`));
95
+ console.log('Opening your browser to authorize this device...\n');
96
+ console.log(chalk.dim(`If the browser doesn't open, visit: ${authData.verification_url}\n`));
97
+
98
+ // Open browser with the code pre-filled
99
+ const verifyUrl = `${authData.verification_url}?code=${authData.user_code}`;
100
+ await open(verifyUrl).catch(() => {
101
+ // Browser failed to open, user will have to do it manually
102
+ });
103
+
104
+ // Step 3: Poll for approval
105
+ spinner.start('Waiting for authorization...');
106
+
107
+ const result = await pollForApproval(
108
+ apiUrl,
109
+ authData.device_code,
110
+ authData.interval,
111
+ authData.expires_in
112
+ );
113
+
114
+ // Step 4: Save credentials
115
+ setApiKey(result.access_token);
116
+ setApiUrl(apiUrl);
47
117
 
48
- if (result.success) {
49
118
  spinner.succeed(chalk.green('Logged in successfully!'));
50
- console.log('\nRun ' + chalk.cyan('pc setup') + ' to configure auto-capture for your CLI tools.');
51
- } else {
52
- spinner.fail(chalk.red('Connection failed: ' + result.error));
53
- console.log('Check your API key and try again.');
119
+ console.log(`\nWelcome, ${chalk.cyan(result.user_email)}!`);
120
+ console.log('\nRun ' + chalk.cyan('pc setup') + ' to configure auto-capture for your CLI tools.\n');
121
+
122
+ } catch (error) {
123
+ spinner.fail(chalk.red('Login failed: ' + error.message));
124
+ console.log('\nPlease try again or visit ' + chalk.cyan(apiUrl) + ' to sign up.\n');
54
125
  }
55
126
  }
56
127