qubitlink_cli 1.0.0-dev.1
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/LICENSE +7 -0
- package/README.md +53 -0
- package/dist/config.js +39 -0
- package/dist/env.js +1 -0
- package/dist/index.js +115 -0
- package/dist/services/api.js +28 -0
- package/dist/services/auth.js +105 -0
- package/dist/services/link.js +31 -0
- package/dist/services/org.js +41 -0
- package/dist/utils/html.js +47 -0
- package/package.json +48 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
ISC License (ISC)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026, Pyae Phyo Maung
|
|
4
|
+
|
|
5
|
+
Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies.
|
|
6
|
+
|
|
7
|
+
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# QubitLink CLI 🚀
|
|
2
|
+
|
|
3
|
+
Manage your QubitLink short links and organizations directly from your terminal.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
Install the CLI globally using npm:
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install -g qubitlink_cli
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Usage
|
|
14
|
+
|
|
15
|
+
### 1. Login
|
|
16
|
+
Authenticate your account via browser:
|
|
17
|
+
```bash
|
|
18
|
+
qubit login
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
### 2. Manage Scope
|
|
22
|
+
Switch between Personal and Organization contexts:
|
|
23
|
+
```bash
|
|
24
|
+
qubit scope
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
### 3. Create a short link
|
|
28
|
+
```bash
|
|
29
|
+
qubit link create https://example.com --title "My Awesome Link"
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
### 4. List your links
|
|
33
|
+
```bash
|
|
34
|
+
qubit link list
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
### 5. Check session
|
|
38
|
+
```bash
|
|
39
|
+
qubit whoami
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
### 6. Logout
|
|
43
|
+
```bash
|
|
44
|
+
qubit logout
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## Features
|
|
48
|
+
- **Secure Browser OAuth**: No need to handle sensitive API keys manually.
|
|
49
|
+
- **Organization Support**: Seamlessly switch between personal and organization projects.
|
|
50
|
+
- **Fast & Lightweight**: Built with Node.js for maximum performance.
|
|
51
|
+
|
|
52
|
+
## License
|
|
53
|
+
ISC
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import Conf from 'conf';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import fs from 'fs';
|
|
4
|
+
import { fileURLToPath } from 'url';
|
|
5
|
+
import { ENV } from './env.js';
|
|
6
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
7
|
+
// Load environment variables manually to avoid verbose logging from dotenv/dotenvx
|
|
8
|
+
const envFile = ENV === 'prod' ? '.env.production' : '.env.development';
|
|
9
|
+
const envPath = path.resolve(process.cwd(), envFile);
|
|
10
|
+
if (fs.existsSync(envPath)) {
|
|
11
|
+
const envContent = fs.readFileSync(envPath, 'utf8');
|
|
12
|
+
envContent.split('\n').forEach(line => {
|
|
13
|
+
const [key, ...valueParts] = line.split('=');
|
|
14
|
+
if (key && valueParts.length > 0) {
|
|
15
|
+
const value = valueParts.join('=').trim().replace(/^["']|["']$/g, '');
|
|
16
|
+
process.env[key.trim()] = value;
|
|
17
|
+
}
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
const CONFIGS = {
|
|
21
|
+
dev: {
|
|
22
|
+
webAppUrl: process.env.QUBIT_WEB_APP_URL || 'https://dev.app.qubitlink.co',
|
|
23
|
+
apiBaseUrl: process.env.QUBIT_API_BASE_URL || 'https://dev-core.qubitlink.co/api',
|
|
24
|
+
clientId: process.env.QUBIT_CLIENT_ID || 'ql_8a1b00a134afe05787b8c787b921cf78',
|
|
25
|
+
},
|
|
26
|
+
prod: {
|
|
27
|
+
webAppUrl: process.env.QUBIT_WEB_APP_URL || 'https://app.qubitlink.co',
|
|
28
|
+
apiBaseUrl: process.env.QUBIT_API_BASE_URL || 'https://core.qubitlink.co/api',
|
|
29
|
+
clientId: process.env.QUBIT_CLIENT_ID || 'ql_prod_placeholder_id',
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
const persistentConfig = new Conf({ projectName: 'qubitlink-cli' });
|
|
33
|
+
export function getEnv() {
|
|
34
|
+
return ENV;
|
|
35
|
+
}
|
|
36
|
+
export function getConfig() {
|
|
37
|
+
return CONFIGS[ENV];
|
|
38
|
+
}
|
|
39
|
+
export { persistentConfig as config };
|
package/dist/env.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const ENV = 'dev';
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { Command } from 'commander';
|
|
3
|
+
import chalk from 'chalk';
|
|
4
|
+
import inquirer from 'inquirer';
|
|
5
|
+
import { AuthService } from './services/auth.js';
|
|
6
|
+
import { LinkService } from './services/link.js';
|
|
7
|
+
import { OrgService } from './services/org.js';
|
|
8
|
+
const program = new Command();
|
|
9
|
+
program
|
|
10
|
+
.name('qubit')
|
|
11
|
+
.description('QubitLink CLI - Manage your short links from the terminal')
|
|
12
|
+
.version('1.0.0');
|
|
13
|
+
// --- Auth Commands ---
|
|
14
|
+
program
|
|
15
|
+
.command('login')
|
|
16
|
+
.description('Login to your QubitLink account (via browser)')
|
|
17
|
+
.action(async () => {
|
|
18
|
+
try {
|
|
19
|
+
await AuthService.loginFlow();
|
|
20
|
+
console.log(chalk.green('\nLogin successful!'));
|
|
21
|
+
const user = AuthService.getUser();
|
|
22
|
+
console.log(`Welcome back, ${chalk.bold(user.fullname)}.`);
|
|
23
|
+
process.exit(0);
|
|
24
|
+
}
|
|
25
|
+
catch (error) {
|
|
26
|
+
console.error(chalk.red('\nLogin aborted:'), error.message);
|
|
27
|
+
}
|
|
28
|
+
});
|
|
29
|
+
program
|
|
30
|
+
.command('logout')
|
|
31
|
+
.description('Logout and clear local session')
|
|
32
|
+
.action(() => {
|
|
33
|
+
AuthService.logout();
|
|
34
|
+
});
|
|
35
|
+
program
|
|
36
|
+
.command('whoami')
|
|
37
|
+
.description('Show current active session')
|
|
38
|
+
.action(() => {
|
|
39
|
+
const user = AuthService.getUser();
|
|
40
|
+
if (!user) {
|
|
41
|
+
console.log(chalk.yellow('You are not logged in. Run "qubit login" to start.'));
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
const activeOrg = AuthService.getActiveOrgId();
|
|
45
|
+
console.log(chalk.cyan('Logged in as:'), chalk.bold(user.email));
|
|
46
|
+
console.log(chalk.cyan('Active Scope:'), activeOrg ? chalk.green(`Organization (${activeOrg})`) : chalk.yellow('Personal'));
|
|
47
|
+
});
|
|
48
|
+
// --- Link Commands ---
|
|
49
|
+
const link = program.command('link').description('Manage short links');
|
|
50
|
+
link
|
|
51
|
+
.command('create <url>')
|
|
52
|
+
.description('Create a new short link')
|
|
53
|
+
.option('-t, --title <title>', 'Title of the link')
|
|
54
|
+
.option('-s, --slug <slug>', 'Custom short code (slug)')
|
|
55
|
+
.option('-p, --password <password>', 'Protect link with password')
|
|
56
|
+
.option('--sensitive', 'Mark as sensitive content')
|
|
57
|
+
.option('--org <orgId>', 'Create in specific organization')
|
|
58
|
+
.action(async (url, options) => {
|
|
59
|
+
try {
|
|
60
|
+
console.log(chalk.blue('Creating link...'));
|
|
61
|
+
const result = await LinkService.create(url, options);
|
|
62
|
+
console.log(chalk.green('\nLink created successfully!'));
|
|
63
|
+
console.log(chalk.bold('Short URL:'), result.data.shortUrl);
|
|
64
|
+
}
|
|
65
|
+
catch (error) {
|
|
66
|
+
console.error(chalk.red('Failed to create link:'), error.response?.data?.message || error.message);
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
link
|
|
70
|
+
.command('list')
|
|
71
|
+
.description('List your links')
|
|
72
|
+
.action(async () => {
|
|
73
|
+
try {
|
|
74
|
+
const result = await LinkService.list();
|
|
75
|
+
const links = result.data || [];
|
|
76
|
+
console.log(chalk.cyan('\n--- Your Links ---'));
|
|
77
|
+
if (links.length === 0) {
|
|
78
|
+
console.log('No links found.');
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
links.forEach((l) => {
|
|
82
|
+
console.log(`${chalk.bold(l.urlKey)} -> ${l.originalURL || '[Page]'} (${l.clicks || 0} clicks)`);
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
catch (error) {
|
|
86
|
+
console.error(chalk.red('Failed to list links:'), error.response?.data?.message || error.message);
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
// --- Scope Commands ---
|
|
90
|
+
const scope = program.command('scope').description('Manage organization or personal context');
|
|
91
|
+
scope
|
|
92
|
+
.action(async () => {
|
|
93
|
+
try {
|
|
94
|
+
const orgs = await OrgService.list();
|
|
95
|
+
const choices = [
|
|
96
|
+
{ name: 'Personal Mode', value: null },
|
|
97
|
+
...orgs.map((o) => ({ name: `${o.name} (Organization)`, value: o._id }))
|
|
98
|
+
];
|
|
99
|
+
const { scopeId } = await inquirer.prompt([
|
|
100
|
+
{
|
|
101
|
+
type: 'list',
|
|
102
|
+
name: 'scopeId',
|
|
103
|
+
message: 'Select active scope:',
|
|
104
|
+
choices,
|
|
105
|
+
default: AuthService.getActiveOrgId()
|
|
106
|
+
}
|
|
107
|
+
]);
|
|
108
|
+
AuthService.setActiveOrg(scopeId);
|
|
109
|
+
console.log(chalk.green(`\nActive scope set to: ${chalk.bold(scopeId ? 'Organization' : 'Personal')}`));
|
|
110
|
+
}
|
|
111
|
+
catch (error) {
|
|
112
|
+
console.error(chalk.red('Failed to manage scope:'), error.response?.data?.message || error.message);
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
program.parse(process.argv);
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import axios from 'axios';
|
|
2
|
+
import { getConfig, config } from '../config.js';
|
|
3
|
+
const api = axios.create({
|
|
4
|
+
headers: {
|
|
5
|
+
'Content-Type': 'application/json',
|
|
6
|
+
},
|
|
7
|
+
});
|
|
8
|
+
// Dynamically set baseURL on each request based on current env
|
|
9
|
+
api.interceptors.request.use((req) => {
|
|
10
|
+
const { apiBaseUrl } = getConfig();
|
|
11
|
+
if (!req.baseURL) {
|
|
12
|
+
req.baseURL = apiBaseUrl;
|
|
13
|
+
}
|
|
14
|
+
const token = config.get('accessToken');
|
|
15
|
+
if (token) {
|
|
16
|
+
req.headers.Authorization = `Bearer ${token}`;
|
|
17
|
+
}
|
|
18
|
+
const activeOrgId = config.get('activeOrgId');
|
|
19
|
+
if (activeOrgId) {
|
|
20
|
+
req.headers['x-org-id'] = activeOrgId;
|
|
21
|
+
}
|
|
22
|
+
return req;
|
|
23
|
+
});
|
|
24
|
+
api.interceptors.response.use((response) => response, async (error) => {
|
|
25
|
+
return Promise.reject(error);
|
|
26
|
+
});
|
|
27
|
+
export default api;
|
|
28
|
+
export { config };
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import api, { config } from './api.js';
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
import open from 'open';
|
|
4
|
+
import http from 'http';
|
|
5
|
+
import { URL } from 'url';
|
|
6
|
+
import { getConfig } from '../config.js';
|
|
7
|
+
import { generateAuthResponseHtml } from '../utils/html.js';
|
|
8
|
+
const PORT = 4005;
|
|
9
|
+
const REDIRECT_URI = `http://localhost:${PORT}/callback`;
|
|
10
|
+
export class AuthService {
|
|
11
|
+
static async loginFlow() {
|
|
12
|
+
return new Promise((resolve, reject) => {
|
|
13
|
+
const state = Math.random().toString(36).substring(7);
|
|
14
|
+
const { webAppUrl, clientId } = getConfig();
|
|
15
|
+
const authUrl = `${webAppUrl}/extension/auth?client_id=${clientId}&redirect_uri=${encodeURIComponent(REDIRECT_URI)}&state=${state}`;
|
|
16
|
+
const server = http.createServer(async (req, res) => {
|
|
17
|
+
const finish = (msg, success, err, title) => {
|
|
18
|
+
const html = generateAuthResponseHtml(title || (success ? 'Login Successful' : 'Authentication Failed'), msg, !success);
|
|
19
|
+
res.writeHead(success ? 200 : 400, { 'Content-Type': 'text/html' });
|
|
20
|
+
res.end(html);
|
|
21
|
+
// Short delay to ensure response is sent before closing
|
|
22
|
+
setTimeout(() => {
|
|
23
|
+
server.close();
|
|
24
|
+
res.socket?.destroy();
|
|
25
|
+
if (success)
|
|
26
|
+
resolve();
|
|
27
|
+
else
|
|
28
|
+
reject(err || new Error(msg));
|
|
29
|
+
}, 100);
|
|
30
|
+
};
|
|
31
|
+
try {
|
|
32
|
+
const url = new URL(req.url || '', `http://localhost:${PORT}`);
|
|
33
|
+
if (url.pathname === '/callback') {
|
|
34
|
+
const code = url.searchParams.get('code');
|
|
35
|
+
const receivedState = url.searchParams.get('state');
|
|
36
|
+
if (receivedState !== state) {
|
|
37
|
+
return finish('Security check failed. You may have closed the browser too early or are using an outdated link. Please try again.', false, new Error('State mismatch'), 'Security Check Failed');
|
|
38
|
+
}
|
|
39
|
+
if (code) {
|
|
40
|
+
const success = await this.exchangeCode(code);
|
|
41
|
+
if (success) {
|
|
42
|
+
finish('You have successfully authenticated with QubitLink. You can now close this window and return to the terminal to start using the CLI.', true);
|
|
43
|
+
}
|
|
44
|
+
else {
|
|
45
|
+
finish('We encountered an issue while exchanging your authentication code. Please check your internet connection and try again.', false, new Error('Token exchange failed'), 'Token Exchange Error');
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
else {
|
|
49
|
+
finish('No authorization code was provided by the authentication server. Please try logging in again.', false, new Error('Authorization code missing'), 'Authorization Missing');
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
catch (err) {
|
|
54
|
+
finish('An unexpected error occurred during the authentication process. Please try again later.', false, err, 'Unexpected Error');
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
server.listen(PORT, async () => {
|
|
58
|
+
console.log(chalk.blue('\nOpening browser for authentication...'));
|
|
59
|
+
console.log(chalk.gray(`URL: ${authUrl}\n`));
|
|
60
|
+
try {
|
|
61
|
+
await open(authUrl);
|
|
62
|
+
}
|
|
63
|
+
catch (e) {
|
|
64
|
+
console.log(chalk.yellow('Could not open browser automatically. Please open the link above manually.'));
|
|
65
|
+
}
|
|
66
|
+
console.log(chalk.yellow('Waiting for authentication in browser...'));
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
static async exchangeCode(code) {
|
|
71
|
+
try {
|
|
72
|
+
const response = await api.post('/auth/extension/exchange', { code });
|
|
73
|
+
const { accessToken, refreshToken, user } = response.data;
|
|
74
|
+
config.set('accessToken', accessToken);
|
|
75
|
+
config.set('refreshToken', refreshToken);
|
|
76
|
+
config.set('user', user);
|
|
77
|
+
if (user.organizationId) {
|
|
78
|
+
config.set('activeOrgId', user.organizationId);
|
|
79
|
+
}
|
|
80
|
+
return true;
|
|
81
|
+
}
|
|
82
|
+
catch (error) {
|
|
83
|
+
console.error(chalk.red('\nToken exchange failed:'), error.response?.data?.message || error.message);
|
|
84
|
+
return false;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
static logout() {
|
|
88
|
+
config.clear();
|
|
89
|
+
console.log(chalk.yellow('Logged out successfully.'));
|
|
90
|
+
}
|
|
91
|
+
static getUser() {
|
|
92
|
+
return config.get('user');
|
|
93
|
+
}
|
|
94
|
+
static getActiveOrgId() {
|
|
95
|
+
return config.get('activeOrgId');
|
|
96
|
+
}
|
|
97
|
+
static setActiveOrg(orgId) {
|
|
98
|
+
if (orgId === null) {
|
|
99
|
+
config.delete('activeOrgId');
|
|
100
|
+
}
|
|
101
|
+
else {
|
|
102
|
+
config.set('activeOrgId', orgId);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import api from './api.js';
|
|
2
|
+
import { AuthService } from './auth.js';
|
|
3
|
+
export class LinkService {
|
|
4
|
+
static async list(page = 1, limit = 20) {
|
|
5
|
+
const activeOrgId = AuthService.getActiveOrgId();
|
|
6
|
+
const params = {
|
|
7
|
+
page,
|
|
8
|
+
limit,
|
|
9
|
+
scope: activeOrgId ? 'organization' : 'personal'
|
|
10
|
+
};
|
|
11
|
+
const response = await api.get('/short-url', { params });
|
|
12
|
+
// Dashboard API returns { success: true, data: [...], pagination: {...} }
|
|
13
|
+
return response.data;
|
|
14
|
+
}
|
|
15
|
+
static async create(url, options) {
|
|
16
|
+
const activeOrgId = AuthService.getActiveOrgId();
|
|
17
|
+
const payload = {
|
|
18
|
+
originalURL: url,
|
|
19
|
+
title: options.title,
|
|
20
|
+
urlKey: options.urlKey,
|
|
21
|
+
scope: activeOrgId ? 'organization' : 'personal'
|
|
22
|
+
};
|
|
23
|
+
const response = await api.post('/short-url', payload);
|
|
24
|
+
// Dashboard API returns { success: true, data: { ... }, message: "..." }
|
|
25
|
+
return response.data;
|
|
26
|
+
}
|
|
27
|
+
static async view(urlKey) {
|
|
28
|
+
const response = await api.get(`/short-url/${urlKey}`);
|
|
29
|
+
return response.data;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import api, { config } from './api.js';
|
|
2
|
+
export class OrgService {
|
|
3
|
+
static async list() {
|
|
4
|
+
try {
|
|
5
|
+
// 1. Try to get current user profile from API first
|
|
6
|
+
let user = null;
|
|
7
|
+
try {
|
|
8
|
+
const meResponse = await api.get('/user/me');
|
|
9
|
+
user = meResponse.data.data;
|
|
10
|
+
}
|
|
11
|
+
catch (err) {
|
|
12
|
+
// Ignore API error for /user/me and fallback to cache
|
|
13
|
+
}
|
|
14
|
+
// 2. Fallback to cached user if API fails or doesn't have orgId
|
|
15
|
+
if (!user || !user.organizationId) {
|
|
16
|
+
user = config.get('user');
|
|
17
|
+
}
|
|
18
|
+
if (!user || !user.organizationId) {
|
|
19
|
+
return [];
|
|
20
|
+
}
|
|
21
|
+
// 3. Fetch organization details
|
|
22
|
+
const orgResponse = await api.get(`/organizations/${user.organizationId}`);
|
|
23
|
+
const org = orgResponse.data.organization;
|
|
24
|
+
if (!org) {
|
|
25
|
+
return [];
|
|
26
|
+
}
|
|
27
|
+
// Return as array
|
|
28
|
+
return [
|
|
29
|
+
{
|
|
30
|
+
_id: org._id,
|
|
31
|
+
name: org.name,
|
|
32
|
+
slug: org.slug
|
|
33
|
+
}
|
|
34
|
+
];
|
|
35
|
+
}
|
|
36
|
+
catch (error) {
|
|
37
|
+
console.error('Failed to fetch organization details:', error);
|
|
38
|
+
return []; // Return empty instead of throwing to prevent CLI crash
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
export function generateAuthResponseHtml(title, message, isError = false) {
|
|
2
|
+
const icon = isError
|
|
3
|
+
? `<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6 text-red-600"><path stroke-linecap="round" stroke-linejoin="round" d="M12 9v3.75m9-.75a9 9 0 1 1-18 0 9 9 0 0 1 18 0Zm-9 3.75h.008v.008H12v-.008Z" /></svg>`
|
|
4
|
+
: `<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6 text-indigo-600"><path stroke-linecap="round" stroke-linejoin="round" d="M9 12.75 11.25 15 15 9.75M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z" /></svg>`;
|
|
5
|
+
const iconBg = isError ? 'bg-red-100' : 'bg-indigo-100';
|
|
6
|
+
return `
|
|
7
|
+
<!DOCTYPE html>
|
|
8
|
+
<html lang="en">
|
|
9
|
+
<head>
|
|
10
|
+
<meta charset="UTF-8">
|
|
11
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
12
|
+
<title>QubitLink Authentication</title>
|
|
13
|
+
<script src="https://cdn.tailwindcss.com"></script>
|
|
14
|
+
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700&display=swap" rel="stylesheet">
|
|
15
|
+
<style>
|
|
16
|
+
body { font-family: 'Inter', sans-serif; }
|
|
17
|
+
.qubit-txt {
|
|
18
|
+
-webkit-text-fill-color: transparent;
|
|
19
|
+
background: linear-gradient(90deg, #39319B, #6543B0, #BF6DDC) 0 0 / 100% 200%;
|
|
20
|
+
-webkit-background-clip: text;
|
|
21
|
+
background-clip: text;
|
|
22
|
+
}
|
|
23
|
+
</style>
|
|
24
|
+
</head>
|
|
25
|
+
<body class="min-h-screen bg-gradient-to-b from-white via-purple-100 to-purple-400 flex items-center justify-center px-4 py-12">
|
|
26
|
+
<div class="w-full max-w-md">
|
|
27
|
+
<div class="flex items-center justify-center mb-8 scale-110">
|
|
28
|
+
<img class="h-10 w-auto mr-3" alt="Qubit Link Logo" src="data:image/svg+xml,%3csvg%20width='1014'%20height='963'%20viewBox='0%200%201014%20963'%20fill='none'%20xmlns='http://www.w3.org/2000/svg'%3e%3cpath%20d='M444.617%20287.193C480.847%20279.923%20518.42%20283.559%20552.583%20297.642C586.747%20311.725%20615.968%20335.623%20636.55%20366.312C655.444%20394.484%20666.278%20427.219%20667.977%20460.983H667.999V461.392C668.137%20464.274%20668.209%20467.164%20668.214%20470.059H667.999V775.603H668.405C668.434%20793.017%20673.627%20810.033%20683.327%20824.496C693.027%20838.96%20706.798%20850.221%20722.899%20856.858C738.999%20863.495%20756.706%20865.21%20773.781%20861.783C790.855%20858.357%20806.53%20849.944%20818.824%20837.608C831.117%20825.273%20839.477%20809.57%20842.845%20792.483C846.214%20775.397%20844.439%20757.696%20837.748%20741.618C831.056%20725.54%20819.747%20711.808%20805.25%20702.157C790.754%20692.507%20773.721%20687.371%20756.306%20687.4L756.138%20588.617C793.091%20588.554%20829.232%20599.45%20859.992%20619.928C890.751%20640.405%20914.749%20669.545%20928.948%20703.66C943.147%20737.776%20946.91%20775.335%20939.763%20811.59C932.616%20847.845%20914.878%20881.166%20888.793%20907.34C862.709%20933.514%20829.447%20951.365%20793.217%20958.636C756.987%20965.906%20719.414%20962.27%20685.25%20948.188C651.087%20934.104%20621.867%20910.206%20601.285%20879.517C580.703%20848.827%20569.684%20812.723%20569.621%20775.771L569.999%20775.77V470.227L569.43%20470.228C569.4%20452.813%20564.207%20435.797%20554.507%20421.334C544.807%20406.87%20531.037%20395.608%20514.936%20388.971C498.836%20382.334%20481.129%20380.62%20464.054%20384.047C446.98%20387.473%20431.304%20395.887%20419.011%20408.222C406.718%20420.557%20398.358%20436.26%20394.99%20453.346C391.621%20470.432%20393.396%20488.134%20400.087%20504.212C406.779%20520.29%20418.088%20534.022%20432.584%20543.673C447.081%20553.323%20464.114%20558.458%20481.529%20558.429L481.697%20657.213C444.744%20657.276%20408.603%20646.379%20377.843%20625.901C347.083%20605.424%20323.086%20576.285%20308.887%20542.17C294.688%20508.054%20290.924%20470.494%20298.072%20434.239C305.219%20397.984%20322.957%20364.663%20349.041%20338.489C375.126%20312.316%20408.387%20294.464%20444.617%20287.193ZM421.553%203.68848C524.376%20-9.11765%20628.585%2011.6217%20718.669%2062.8203C808.754%20114.019%20879.897%20192.939%20921.509%20287.833C956.537%20367.713%20969.063%20455.317%20958.212%20541.252C954.822%20568.1%20928.237%20584.523%20901.873%20578.421L875.503%20572.317C849.139%20566.215%20833.074%20539.872%20835.5%20512.919C840.858%20453.422%20831.171%20393.249%20806.97%20338.06C776.178%20267.839%20723.533%20209.439%20656.873%20171.553C590.211%20133.667%20513.097%20118.32%20437.01%20127.796C360.923%20137.272%20289.93%20171.066%20234.598%20224.146C179.267%20277.226%20142.555%20346.755%20129.927%20422.383C117.3%20498.011%20129.433%20575.695%20164.519%20643.872C199.605%20712.049%20255.769%20767.073%20324.65%20800.754C378.787%20827.225%20438.506%20839.402%20498.174%20836.52C525.204%20835.214%20550.857%20852.358%20555.86%20878.953L560.864%20905.554C565.866%20932.149%20548.354%20958.03%20521.388%20960.303C435.077%20967.576%20348.068%20951.423%20269.711%20913.108C176.627%20867.592%20100.728%20793.234%2053.3139%20701.102C5.89967%20608.969%20-10.4963%20503.988%206.56784%20401.786C23.6321%20299.584%2073.2442%20205.625%20148.018%20133.894C222.792%2062.1623%20318.73%2016.4947%20421.553%203.68848Z'%20fill='url(%23paint0_linear_14_2)'/%3e%3cdefs%3e%3clinearGradient%20id='paint0_linear_14_2'%20x1='480.997'%20y1='-11.0172'%20x2='480.997'%20y2='962.273'%20gradientUnits='userSpaceOnUse'%3e%3cstop%20offset='0.302885'%20stop-color='%233A319B'/%3e%3cstop%20offset='1'%20stop-color='%23D175E5'/%3e%3c/linearGradient%3e%3c/defs%3e%3c/svg%3e">
|
|
29
|
+
<span class="text-3xl font-bold text-indigo-700 qubit-txt">QubitLink.</span>
|
|
30
|
+
</div>
|
|
31
|
+
<div class="rounded-3xl border border-gray-100 bg-white/90 backdrop-blur-md shadow-2xl p-8 transform transition-all hover:scale-[1.01]">
|
|
32
|
+
<div class="flex items-center mb-6">
|
|
33
|
+
<div class="w-14 h-14 rounded-2xl ${iconBg} flex items-center justify-center shadow-inner">
|
|
34
|
+
${icon}
|
|
35
|
+
</div>
|
|
36
|
+
<h2 class="ml-4 text-3xl font-extrabold text-gray-900 leading-tight">${title}</h2>
|
|
37
|
+
</div>
|
|
38
|
+
<p class="text-lg text-gray-600 leading-relaxed">${message}</p>
|
|
39
|
+
<div class="mt-8 pt-6 border-t border-gray-100 italic text-sm text-gray-400">
|
|
40
|
+
You can safely close this window now.
|
|
41
|
+
</div>
|
|
42
|
+
</div>
|
|
43
|
+
</div>
|
|
44
|
+
</body>
|
|
45
|
+
</html>
|
|
46
|
+
`;
|
|
47
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "qubitlink_cli",
|
|
3
|
+
"version": "1.0.0-dev.1",
|
|
4
|
+
"description": "CLI tool for QubitLink",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"bin": {
|
|
8
|
+
"qubit": "./dist/index.js"
|
|
9
|
+
},
|
|
10
|
+
"scripts": {
|
|
11
|
+
"build": "npm run build:dev",
|
|
12
|
+
"build:dev": "node -e \"const fs = require('fs'); fs.writeFileSync('src/env.ts', 'export type QubitEnv = \\'dev\\' | \\'prod\\';\\nexport const ENV: QubitEnv = \\'dev\\';\\n')\" && tsc",
|
|
13
|
+
"build:prod": "node -e \"const fs = require('fs'); fs.writeFileSync('src/env.ts', 'export type QubitEnv = \\'dev\\' | \\'prod\\';\\nexport const ENV: QubitEnv = \\'prod\\';\\n')\" && tsc",
|
|
14
|
+
"publish:dev": "npm run build:dev && npm publish --tag dev",
|
|
15
|
+
"publish:prod": "npm run build:prod && npm publish",
|
|
16
|
+
"start": "node dist/index.js",
|
|
17
|
+
"dev": "ts-node src/index.ts",
|
|
18
|
+
"test": "echo \"Error: no test specified\" && exit 1"
|
|
19
|
+
},
|
|
20
|
+
"files": [
|
|
21
|
+
"dist",
|
|
22
|
+
"README.md",
|
|
23
|
+
"LICENSE"
|
|
24
|
+
],
|
|
25
|
+
"keywords": [
|
|
26
|
+
"qubitlink",
|
|
27
|
+
"cli",
|
|
28
|
+
"url-shortener"
|
|
29
|
+
],
|
|
30
|
+
"author": "Studio Next Steps LLC",
|
|
31
|
+
"license": "ISC",
|
|
32
|
+
"dependencies": {
|
|
33
|
+
"@types/open": "^6.1.0",
|
|
34
|
+
"axios": "^1.7.9",
|
|
35
|
+
"chalk": "^5.4.1",
|
|
36
|
+
"commander": "^13.1.0",
|
|
37
|
+
"conf": "^13.1.0",
|
|
38
|
+
"inquirer": "^12.3.2",
|
|
39
|
+
"open": "^11.0.0"
|
|
40
|
+
},
|
|
41
|
+
"devDependencies": {
|
|
42
|
+
"@types/chalk": "^2.2.4",
|
|
43
|
+
"@types/inquirer": "^9.0.7",
|
|
44
|
+
"@types/node": "^22.19.7",
|
|
45
|
+
"ts-node": "^10.9.2",
|
|
46
|
+
"typescript": "^5.7.3"
|
|
47
|
+
}
|
|
48
|
+
}
|