mirror of
https://github.com/glitternetwork/pinme.git
synced 2026-08-28 17:42:38 +08:00
Initial commit for new repository
This commit is contained in:
@@ -0,0 +1,7 @@
|
||||
IPFS_API_URL=https://example.com
|
||||
IPFS_PREVIEW_URL=https://example.com
|
||||
|
||||
SECRET_KEY=your-secret-key
|
||||
|
||||
FILE_SIZE_LIMIT=100 # MB
|
||||
DIRECTORY_SIZE_LIMIT=500 # MB
|
||||
@@ -0,0 +1,4 @@
|
||||
node_modules
|
||||
/dist
|
||||
.DS_Store
|
||||
.env
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
node_modules
|
||||
/bin
|
||||
/src
|
||||
.github
|
||||
.gitignore
|
||||
.prettierrc
|
||||
rollup.config.js
|
||||
tsconfig.json
|
||||
*.log
|
||||
.env
|
||||
@@ -0,0 +1,2 @@
|
||||
/dist
|
||||
*.yaml
|
||||
@@ -0,0 +1,19 @@
|
||||
module.exports = {
|
||||
pluginSearchDirs: false,
|
||||
plugins: [
|
||||
require.resolve('prettier-plugin-organize-imports'),
|
||||
require.resolve('prettier-plugin-packagejson'),
|
||||
],
|
||||
printWidth: 80,
|
||||
proseWrap: 'never',
|
||||
singleQuote: true,
|
||||
trailingComma: 'all',
|
||||
overrides: [
|
||||
{
|
||||
files: '*.md',
|
||||
options: {
|
||||
proseWrap: 'preserve',
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2025 PINME
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -0,0 +1,161 @@
|
||||
# PinMe
|
||||
|
||||
[PinMe](https://pinme.eth.limo/) is a simple and easy-to-use command-line tool for uploading files and directories to the [IPFS](https://ipfs.tech/) network.
|
||||
|
||||
## Features
|
||||
|
||||
- 🚀 Quickly upload files and directories to IPFS
|
||||
- 📂 Support for various file types and sizes
|
||||
- 📊 View and manage upload history
|
||||
- 🔗 Automatically generate accessible IPFS links
|
||||
- 🌐 Preview uploaded content
|
||||
|
||||
## Installation
|
||||
|
||||
### Using npm
|
||||
|
||||
```bash
|
||||
npm install -g pinme
|
||||
```
|
||||
|
||||
### Using yarn
|
||||
|
||||
```bash
|
||||
yarn global add pinme
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
### Upload files or directories
|
||||
|
||||
```bash
|
||||
# Interactive upload
|
||||
pinme upload
|
||||
|
||||
# Specify path directly
|
||||
pinme upload /path/to/file-or-directory
|
||||
```
|
||||
|
||||
### View upload history
|
||||
|
||||
```bash
|
||||
# Show the last 10 upload records
|
||||
pinme list
|
||||
|
||||
# Or use the shorthand command
|
||||
pinme ls
|
||||
|
||||
# Limit the number of records shown
|
||||
pinme list -l 5
|
||||
|
||||
# Clear all upload history
|
||||
pinme list -c
|
||||
```
|
||||
|
||||
### Get help
|
||||
|
||||
```bash
|
||||
# Display help information
|
||||
pinme help
|
||||
```
|
||||
|
||||
## Command Details
|
||||
|
||||
### `upload`
|
||||
|
||||
Upload a file or directory to the IPFS network.
|
||||
|
||||
```bash
|
||||
pinme upload [path]
|
||||
```
|
||||
|
||||
**Options:**
|
||||
- `path`: Path to the file or directory to upload (optional, if not provided, interactive mode will be entered)
|
||||
|
||||
**Examples:**
|
||||
```bash
|
||||
# Interactive upload
|
||||
pinme upload
|
||||
|
||||
# Upload a specific file
|
||||
pinme upload ./example.jpg
|
||||
|
||||
# Upload an entire directory
|
||||
pinme upload ./my-website
|
||||
```
|
||||
|
||||
### `list` / `ls`
|
||||
|
||||
Display upload history.
|
||||
|
||||
```bash
|
||||
pinme list [options]
|
||||
pinme ls [options]
|
||||
```
|
||||
|
||||
**Options:**
|
||||
- `-l, --limit <number>`: Limit the number of records displayed
|
||||
- `-c, --clear`: Clear all upload history
|
||||
|
||||
**Examples:**
|
||||
```bash
|
||||
# Show the last 10 records
|
||||
pinme list
|
||||
|
||||
# Show the last 5 records
|
||||
pinme ls -l 5
|
||||
|
||||
# Clear all history records
|
||||
pinme list -c
|
||||
```
|
||||
|
||||
### `help`
|
||||
|
||||
Display help information.
|
||||
|
||||
```bash
|
||||
pinme help [command]
|
||||
```
|
||||
|
||||
**Options:**
|
||||
- `command`: The specific command to view help for (optional)
|
||||
|
||||
**Examples:**
|
||||
```bash
|
||||
# Display general help
|
||||
pinme help
|
||||
```
|
||||
|
||||
## Upload Limits
|
||||
|
||||
- Single file size limit: 100MB
|
||||
- Total directory size limit: 500MB
|
||||
|
||||
## File Storage
|
||||
|
||||
Uploaded files are stored on the IPFS network and accessible through the Glitter Protocol's IPFS gateway. After a successful upload, you will receive:
|
||||
|
||||
1. IPFS hash value
|
||||
2. Accessible URL link
|
||||
|
||||
### Log Locations
|
||||
|
||||
Logs and configuration files are stored in:
|
||||
- Linux/macOS: `~/.pinme/`
|
||||
- Windows: `%USERPROFILE%\.pinme\`
|
||||
|
||||
|
||||
## License
|
||||
|
||||
MIT License - See the [LICENSE](LICENSE) file for details
|
||||
|
||||
## Contact Us
|
||||
|
||||
If you have questions or suggestions, please contact us through:
|
||||
|
||||
- GitHub Issues: [https://github.com/glitternetwork/pinme/issues](https://github.com/glitternetwork/pinme/issue)
|
||||
- Email: [pinme@glitterprotocol.io](mailto:pinme@glitterprotocol.io)
|
||||
|
||||
---
|
||||
|
||||
Developed and maintained by the [Glitter Protocol](https://glitterprotocol.io/) team
|
||||
@@ -0,0 +1,90 @@
|
||||
import dotenv from 'dotenv';
|
||||
|
||||
dotenv.config();
|
||||
|
||||
import { Command } from "commander";
|
||||
import chalk from "chalk";
|
||||
import figlet from "figlet";
|
||||
import { version } from '../package.json';
|
||||
|
||||
import upload from './upload';
|
||||
import { displayUploadHistory, clearUploadHistory } from './utils/history';
|
||||
|
||||
// display the ASCII art logo
|
||||
function showBanner(): void {
|
||||
console.log(
|
||||
chalk.cyan(
|
||||
figlet.textSync("Pinme", { horizontalLayout: "full" })
|
||||
)
|
||||
);
|
||||
console.log(chalk.cyan("A command-line tool for uploading files to IPFS\n"));
|
||||
}
|
||||
|
||||
const program = new Command();
|
||||
|
||||
program
|
||||
.name("pinme")
|
||||
.version(version)
|
||||
.option('-v, --version', 'output the current version');
|
||||
|
||||
program
|
||||
.command('upload')
|
||||
.description("upload a file or directory to IPFS")
|
||||
.action(() => upload());
|
||||
|
||||
program
|
||||
.command('list')
|
||||
.description("show upload history")
|
||||
.option('-l, --limit <number>', 'limit the number of records to show', parseInt)
|
||||
.option('-c, --clear', 'clear all upload history')
|
||||
.action((options: { limit?: number, clear?: boolean }) => {
|
||||
if (options.clear) {
|
||||
clearUploadHistory();
|
||||
} else {
|
||||
displayUploadHistory(options.limit || 10);
|
||||
}
|
||||
});
|
||||
|
||||
// add ls command as an alias for list command
|
||||
program
|
||||
.command('ls')
|
||||
.description("alias for 'list' command")
|
||||
.option('-l, --limit <number>', 'limit the number of records to show', parseInt)
|
||||
.option('-c, --clear', 'clear all upload history')
|
||||
.action((options: { limit?: number, clear?: boolean }) => {
|
||||
if (options.clear) {
|
||||
clearUploadHistory();
|
||||
} else {
|
||||
displayUploadHistory(options.limit || 10);
|
||||
}
|
||||
});
|
||||
|
||||
// add help command
|
||||
program
|
||||
.command('help')
|
||||
.description("display help information")
|
||||
.action(() => {
|
||||
showBanner();
|
||||
program.help();
|
||||
});
|
||||
|
||||
// custom help output format
|
||||
program.on('--help', () => {
|
||||
console.log('');
|
||||
console.log('Examples:');
|
||||
console.log(' $ pinme upload');
|
||||
console.log(' $ pinme list -l 5');
|
||||
console.log(' $ pinme ls');
|
||||
console.log(' $ pinme help');
|
||||
console.log('');
|
||||
console.log('For more information, visit: https://github.com/glitternetwork/pinme');
|
||||
});
|
||||
|
||||
// parse the command line arguments
|
||||
program.parse(process.argv);
|
||||
|
||||
// If no arguments provided, show banner and help
|
||||
if (process.argv.length === 2) {
|
||||
showBanner();
|
||||
program.help();
|
||||
}
|
||||
+134
@@ -0,0 +1,134 @@
|
||||
import path from 'path';
|
||||
import chalk from 'chalk';
|
||||
import inquirer from 'inquirer';
|
||||
import figlet from "figlet";
|
||||
import upload from './utils/uploadToIpfs';
|
||||
import fs from 'fs';
|
||||
import CryptoJS from 'crypto-js';
|
||||
|
||||
// get from environment variables
|
||||
const URL = process.env.IPFS_PREVIEW_URL;
|
||||
const secretKey = process.env.SECRET_KEY;
|
||||
|
||||
|
||||
// encrypt the hash
|
||||
function encryptHash(hash: string, key: string|undefined): string {
|
||||
try {
|
||||
if (!key) {
|
||||
throw new Error('Secret key not found');
|
||||
}
|
||||
const encrypted = CryptoJS.RC4.encrypt(hash, key).toString();
|
||||
const urlSafe = encrypted
|
||||
.replace(/\+/g, '-')
|
||||
.replace(/\//g, '_')
|
||||
.replace(/=+$/, '');
|
||||
return urlSafe;
|
||||
} catch (error: any) {
|
||||
console.error(`Encryption error: ${error.message}`);
|
||||
return hash;
|
||||
}
|
||||
}
|
||||
|
||||
// create a synchronous path check function
|
||||
function checkPathSync(inputPath: string): string | null {
|
||||
try {
|
||||
// convert to absolute path
|
||||
const absolutePath = path.resolve(inputPath);
|
||||
|
||||
// check if the path exists
|
||||
if (fs.existsSync(absolutePath)) {
|
||||
return absolutePath;
|
||||
}
|
||||
return null;
|
||||
} catch (error: any) {
|
||||
console.error(chalk.red(`error checking path: ${error.message}`));
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
interface UploadOptions {
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
export default async (options?: UploadOptions): Promise<void> => {
|
||||
try {
|
||||
console.log(
|
||||
figlet.textSync("PINME", {
|
||||
font: "Shadow",
|
||||
horizontalLayout: "default",
|
||||
verticalLayout: "default",
|
||||
width: 180,
|
||||
whitespaceBreak: true,
|
||||
})
|
||||
);
|
||||
|
||||
// if the parameter is passed, upload directly, pinme upload /path/to/dir
|
||||
const argPath = process.argv[3];
|
||||
|
||||
if (argPath && !argPath.startsWith('-')) {
|
||||
// use the synchronous path check function
|
||||
const absolutePath = checkPathSync(argPath);
|
||||
if (!absolutePath) {
|
||||
console.log(chalk.red(`path ${argPath} does not exist`));
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(chalk.blue(`uploading ${absolutePath} to ipfs...`));
|
||||
try {
|
||||
const result = await upload(absolutePath);
|
||||
if (result) {
|
||||
const encryptedCID = encryptHash(result.contentHash, secretKey);
|
||||
console.log(chalk.cyan(
|
||||
figlet.textSync("Successful", { horizontalLayout: "full" })
|
||||
))
|
||||
console.log(chalk.cyan(`URL: ${URL}${encryptedCID}`));
|
||||
} else {
|
||||
console.log(chalk.red(`upload failed`));
|
||||
}
|
||||
|
||||
} catch (error: any) {
|
||||
console.error(chalk.red(`error uploading: ${error.message}`));
|
||||
console.error(error.stack);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const answer = await inquirer.prompt([
|
||||
{
|
||||
type: 'input',
|
||||
name: 'path',
|
||||
message: "path to upload: ",
|
||||
},
|
||||
]);
|
||||
|
||||
if (answer.path) {
|
||||
// use the synchronous path check function
|
||||
const absolutePath = checkPathSync(answer.path);
|
||||
if (!absolutePath) {
|
||||
console.log(chalk.red(`path ${answer.path} does not exist`));
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(chalk.blue(`uploading ${absolutePath} to ipfs...`));
|
||||
try {
|
||||
const result = await upload(absolutePath);
|
||||
|
||||
if (result) {
|
||||
const encryptedCID = encryptHash(result.contentHash, secretKey);
|
||||
console.log(chalk.cyan(
|
||||
figlet.textSync("Successful", { horizontalLayout: "full" })
|
||||
))
|
||||
console.log(chalk.cyan(`URL: ${URL}${encryptedCID}`));
|
||||
} else {
|
||||
console.log(chalk.red(`upload failed`));
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error(chalk.red(`error uploading: ${error.message}`));
|
||||
console.error(error.stack);
|
||||
}
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error(chalk.red(`error executing: ${error.message}`));
|
||||
console.error(error.stack);
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,54 @@
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import Inquirer from "inquirer";
|
||||
|
||||
// check if the path exists and return the absolute path
|
||||
function checkPath(inputPath: string): string | null {
|
||||
try {
|
||||
// convert to absolute path
|
||||
const absolutePath = path.resolve(inputPath);
|
||||
|
||||
// check if the path exists
|
||||
if (fs.existsSync(absolutePath)) {
|
||||
return absolutePath;
|
||||
}
|
||||
return null;
|
||||
} catch (error: any) {
|
||||
console.error(`Error checking path: ${error.message}`);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export default async function(projectName: string): Promise<boolean> {
|
||||
// get the current working directory
|
||||
const cwd = process.cwd();
|
||||
// get the project directory
|
||||
const targetDirectory = path.join(cwd, projectName);
|
||||
// check if the directory exists
|
||||
if (fs.existsSync(targetDirectory)) {
|
||||
let { isOverwrite } = await Inquirer.prompt([
|
||||
{
|
||||
name: "isOverwrite", // corresponding to the return value
|
||||
type: "list", // list type
|
||||
message: "Target directory exists, Please choose an action",
|
||||
choices: [
|
||||
{ name: "Overwrite", value: true },
|
||||
{ name: "Cancel", value: false },
|
||||
],
|
||||
},
|
||||
]);
|
||||
// choose Cancel
|
||||
if (!isOverwrite) {
|
||||
console.log("\n Canceled \n");
|
||||
return false;
|
||||
} else {
|
||||
// choose Overwrite, delete the existing directory
|
||||
console.log("Removing folder...");
|
||||
await fs.promises.rm(targetDirectory, { recursive: true, force: true });
|
||||
console.log("Folder removed!");
|
||||
return true
|
||||
}
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,18 @@
|
||||
import CryptoJS from "crypto-js";
|
||||
|
||||
export const decryptHash = (encryptedHash: string | undefined, key: string) => {
|
||||
try {
|
||||
if (!encryptedHash) {
|
||||
return null;
|
||||
}
|
||||
let base64 = encryptedHash.replace(/-/g, "+").replace(/_/g, "/");
|
||||
while (base64.length % 4) {
|
||||
base64 += "=";
|
||||
}
|
||||
const decrypted = CryptoJS.RC4.decrypt(base64, key);
|
||||
return decrypted.toString(CryptoJS.enc.Utf8);
|
||||
} catch (error: any) {
|
||||
console.error(`Decryption error: ${error.message}`);
|
||||
return encryptedHash;
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,21 @@
|
||||
import fs from 'fs-extra';
|
||||
import path from 'path';
|
||||
import os from 'os';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
export function getDeviceId(): string {
|
||||
const configDir = path.join(os.homedir(), '.pinme');
|
||||
const configFile = path.join(configDir, 'device-id');
|
||||
|
||||
if (!fs.existsSync(configDir)) {
|
||||
fs.mkdirSync(configDir, { recursive: true });
|
||||
}
|
||||
|
||||
if (fs.existsSync(configFile)) {
|
||||
return fs.readFileSync(configFile, 'utf8').trim();
|
||||
}
|
||||
|
||||
const deviceId = uuidv4();
|
||||
fs.writeFileSync(configFile, deviceId);
|
||||
return deviceId;
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
import chalk from 'chalk';
|
||||
import figlet from 'figlet';
|
||||
|
||||
// show ASCII art banner
|
||||
function showBanner(): void {
|
||||
console.log(
|
||||
chalk.cyan(
|
||||
figlet.textSync("Pinme CLI", { horizontalLayout: "full" })
|
||||
)
|
||||
);
|
||||
console.log(chalk.cyan("A command-line tool for uploading files to IPFS\n"));
|
||||
}
|
||||
|
||||
// general help
|
||||
function showGeneralHelp(): void {
|
||||
showBanner();
|
||||
|
||||
console.log("USAGE:");
|
||||
console.log(" pinme [command] [options]\n");
|
||||
|
||||
console.log("COMMANDS:");
|
||||
console.log(" upload Upload a file or directory to IPFS");
|
||||
console.log(" list Show upload history");
|
||||
console.log(" ls Alias for 'list' command");
|
||||
console.log(" help [command] Show help for a specific command\n");
|
||||
|
||||
console.log("OPTIONS:");
|
||||
console.log(" -v, --version Output the current version");
|
||||
console.log(" -h, --help Display help for command\n");
|
||||
|
||||
console.log("For more information on a specific command, try:");
|
||||
console.log(" pinme help [command]");
|
||||
}
|
||||
|
||||
// upload command help
|
||||
function showUploadHelp(): void {
|
||||
console.log("COMMAND:");
|
||||
console.log(" upload - Upload a file or directory to IPFS\n");
|
||||
|
||||
console.log("USAGE:");
|
||||
console.log(" pinme upload [path]\n");
|
||||
|
||||
console.log("DESCRIPTION:");
|
||||
console.log(" This command uploads files or directories to IPFS.");
|
||||
console.log(" If no path is provided, it will start in interactive mode.\n");
|
||||
|
||||
console.log("EXAMPLES:");
|
||||
console.log(" pinme upload");
|
||||
console.log(" pinme upload ./my-website\n");
|
||||
|
||||
console.log("LIMITATIONS:");
|
||||
console.log(" - Maximum file size: 10MB");
|
||||
console.log(" - Maximum directory size: 500MB");
|
||||
}
|
||||
|
||||
// list command help
|
||||
function showListHelp(): void {
|
||||
console.log("COMMAND:");
|
||||
console.log(" list - Show upload history\n");
|
||||
|
||||
console.log("USAGE:");
|
||||
console.log(" pinme list [options]\n");
|
||||
|
||||
console.log("OPTIONS:");
|
||||
console.log(" -l, --limit <number> Limit the number of records to show");
|
||||
console.log(" -c, --clear Clear all upload history\n");
|
||||
|
||||
console.log("EXAMPLES:");
|
||||
console.log(" pinme list");
|
||||
console.log(" pinme list -l 5");
|
||||
console.log(" pinme list -c");
|
||||
}
|
||||
|
||||
// ls command help (can reuse the list command help)
|
||||
function showLsHelp(): void {
|
||||
console.log("COMMAND:");
|
||||
console.log(" ls - Alias for 'list' command\n");
|
||||
|
||||
console.log("USAGE:");
|
||||
console.log(" pinme ls [options]\n");
|
||||
|
||||
console.log("OPTIONS:");
|
||||
console.log(" -l, --limit <number> Limit the number of records to show");
|
||||
console.log(" -c, --clear Clear all upload history\n");
|
||||
|
||||
console.log("EXAMPLES:");
|
||||
console.log(" pinme ls");
|
||||
console.log(" pinme ls -l 5");
|
||||
console.log(" pinme ls -c");
|
||||
}
|
||||
|
||||
// show the help for the command
|
||||
function showHelp(command?: string): void {
|
||||
if (!command) {
|
||||
showGeneralHelp();
|
||||
return;
|
||||
}
|
||||
|
||||
switch (command) {
|
||||
case 'upload':
|
||||
showUploadHelp();
|
||||
break;
|
||||
case 'list':
|
||||
showListHelp();
|
||||
break;
|
||||
case 'ls':
|
||||
showLsHelp();
|
||||
break;
|
||||
default:
|
||||
console.log(`Unknown command: ${command}`);
|
||||
showGeneralHelp();
|
||||
}
|
||||
}
|
||||
|
||||
export {
|
||||
showHelp,
|
||||
showBanner
|
||||
};
|
||||
@@ -0,0 +1,149 @@
|
||||
import fs from 'fs-extra';
|
||||
import path from 'path';
|
||||
import os from 'os';
|
||||
import dayjs from 'dayjs';
|
||||
import chalk from 'chalk';
|
||||
import { formatSize } from './uploadLimits';
|
||||
|
||||
// history file path
|
||||
const HISTORY_DIR = path.join(os.homedir(), '.pinme');
|
||||
const HISTORY_FILE = path.join(HISTORY_DIR, 'upload-history.json');
|
||||
|
||||
interface UploadRecord {
|
||||
timestamp: number;
|
||||
date: string;
|
||||
path: string;
|
||||
filename: string;
|
||||
contentHash: string;
|
||||
previewHash: string | null;
|
||||
size: number;
|
||||
fileCount: number;
|
||||
type: 'directory' | 'file';
|
||||
}
|
||||
|
||||
interface UploadHistory {
|
||||
uploads: UploadRecord[];
|
||||
}
|
||||
|
||||
interface UploadData {
|
||||
path: string;
|
||||
filename?: string;
|
||||
contentHash: string;
|
||||
previewHash: string | null;
|
||||
size: number;
|
||||
fileCount?: number;
|
||||
isDirectory?: boolean;
|
||||
}
|
||||
|
||||
// ensure the history directory exists
|
||||
const ensureHistoryDir = (): void => {
|
||||
if (!fs.existsSync(HISTORY_DIR)) {
|
||||
fs.mkdirSync(HISTORY_DIR, { recursive: true });
|
||||
}
|
||||
if (!fs.existsSync(HISTORY_FILE)) {
|
||||
fs.writeJsonSync(HISTORY_FILE, { uploads: [] });
|
||||
}
|
||||
};
|
||||
|
||||
// save the upload history
|
||||
const saveUploadHistory = (uploadData: UploadData): boolean => {
|
||||
try {
|
||||
ensureHistoryDir();
|
||||
|
||||
const history = fs.readJsonSync(HISTORY_FILE) as UploadHistory;
|
||||
|
||||
// add new upload record
|
||||
const newRecord: UploadRecord = {
|
||||
timestamp: Date.now(),
|
||||
date: dayjs().format('YYYY-MM-DD HH:mm:ss'),
|
||||
path: uploadData.path,
|
||||
filename: uploadData.filename || path.basename(uploadData.path),
|
||||
contentHash: uploadData.contentHash,
|
||||
previewHash: uploadData.previewHash,
|
||||
size: uploadData.size,
|
||||
fileCount: uploadData.fileCount || 1,
|
||||
type: uploadData.isDirectory ? 'directory' : 'file'
|
||||
};
|
||||
|
||||
history.uploads.unshift(newRecord); // add to the beginning
|
||||
|
||||
// write to file
|
||||
fs.writeJsonSync(HISTORY_FILE, history, { spaces: 2 });
|
||||
return true;
|
||||
} catch (error: any) {
|
||||
console.error(chalk.red(`Error saving upload history: ${error.message}`));
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
// get the upload history
|
||||
const getUploadHistory = (limit: number = 10): UploadRecord[] => {
|
||||
try {
|
||||
ensureHistoryDir();
|
||||
|
||||
const history = fs.readJsonSync(HISTORY_FILE) as UploadHistory;
|
||||
return history.uploads.slice(0, limit);
|
||||
} catch (error: any) {
|
||||
console.error(chalk.red(`Error reading upload history: ${error.message}`));
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
// display the upload history
|
||||
const displayUploadHistory = (limit: number = 10): void => {
|
||||
const history = getUploadHistory(limit);
|
||||
|
||||
if (history.length === 0) {
|
||||
console.log(chalk.yellow('No upload history found.'));
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(chalk.bold('\n📜 Upload History:'));
|
||||
console.log(chalk.dim('─'.repeat(80)));
|
||||
|
||||
history.forEach((record, index) => {
|
||||
console.log(chalk.bold(`#${index + 1} - ${record.date}`));
|
||||
console.log(chalk.cyan(`Name: ${record.filename}`));
|
||||
console.log(chalk.cyan(`Path: ${record.path}`));
|
||||
console.log(chalk.cyan(`Type: ${record.type}`));
|
||||
console.log(chalk.cyan(`Size: ${formatSize(record.size)}`));
|
||||
if (record.type === 'directory') {
|
||||
console.log(chalk.cyan(`Files: ${record.fileCount}`));
|
||||
}
|
||||
console.log(chalk.cyan(`Content Hash: ${record.contentHash}`));
|
||||
if (record.previewHash) {
|
||||
console.log(chalk.cyan(`Preview Hash: ${record.previewHash}`));
|
||||
console.log(chalk.cyan(`URL: https://ipfs.glitterprotocol.dev/ipfs/${record.previewHash}/#/?from=local`));
|
||||
} else {
|
||||
console.log(chalk.cyan(`URL: https://ipfs.glitterprotocol.dev/ipfs/${record.contentHash}`));
|
||||
}
|
||||
console.log(chalk.dim('─'.repeat(80)));
|
||||
});
|
||||
|
||||
// display the statistics
|
||||
const totalSize = history.reduce((sum, record) => sum + record.size, 0);
|
||||
const totalFiles = history.reduce((sum, record) => sum + record.fileCount, 0);
|
||||
console.log(chalk.bold(`Total Uploads: ${history.length}`));
|
||||
console.log(chalk.bold(`Total Files: ${totalFiles}`));
|
||||
console.log(chalk.bold(`Total Size: ${formatSize(totalSize)}`));
|
||||
};
|
||||
|
||||
// clear the upload history
|
||||
const clearUploadHistory = (): boolean => {
|
||||
try {
|
||||
ensureHistoryDir();
|
||||
fs.writeJsonSync(HISTORY_FILE, { uploads: [] });
|
||||
console.log(chalk.green('Upload history cleared successfully.'));
|
||||
return true;
|
||||
} catch (error: any) {
|
||||
console.error(chalk.red(`Error clearing upload history: ${error.message}`));
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
export {
|
||||
saveUploadHistory,
|
||||
getUploadHistory,
|
||||
displayUploadHistory,
|
||||
clearUploadHistory
|
||||
};
|
||||
@@ -0,0 +1,69 @@
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
|
||||
const FILE_SIZE_LIMIT = parseInt(process.env.FILE_SIZE_LIMIT || '100', 10) * 1024 * 1024; // MB to bytes
|
||||
const DIRECTORY_SIZE_LIMIT = parseInt(process.env.DIRECTORY_SIZE_LIMIT || '500', 10) * 1024 * 1024; // MB to bytes
|
||||
|
||||
interface FileSizeCheck {
|
||||
size: number;
|
||||
exceeds: boolean;
|
||||
limit: number;
|
||||
}
|
||||
|
||||
interface DirectorySizeCheck {
|
||||
size: number;
|
||||
limit: number;
|
||||
exceeds: boolean;
|
||||
}
|
||||
|
||||
function checkFileSizeLimit(filePath: string): FileSizeCheck {
|
||||
const stats = fs.statSync(filePath);
|
||||
return {
|
||||
size: stats.size,
|
||||
limit: FILE_SIZE_LIMIT,
|
||||
exceeds: stats.size > FILE_SIZE_LIMIT
|
||||
};
|
||||
}
|
||||
|
||||
function checkDirectorySizeLimit(directoryPath: string): DirectorySizeCheck {
|
||||
const totalSize = calculateDirectorySize(directoryPath);
|
||||
return {
|
||||
size: totalSize,
|
||||
limit: DIRECTORY_SIZE_LIMIT,
|
||||
exceeds: totalSize > DIRECTORY_SIZE_LIMIT
|
||||
};
|
||||
}
|
||||
|
||||
function calculateDirectorySize(directoryPath: string): number {
|
||||
let totalSize = 0;
|
||||
const files = fs.readdirSync(directoryPath);
|
||||
|
||||
for (const file of files) {
|
||||
const filePath = path.join(directoryPath, file);
|
||||
const stats = fs.statSync(filePath);
|
||||
|
||||
if (stats.isFile()) {
|
||||
totalSize += stats.size;
|
||||
} else if (stats.isDirectory()) {
|
||||
totalSize += calculateDirectorySize(filePath);
|
||||
}
|
||||
}
|
||||
|
||||
return totalSize;
|
||||
}
|
||||
|
||||
function formatSize(bytes: number): string {
|
||||
if (bytes < 1024) return bytes + ' bytes';
|
||||
else if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(2) + ' KB';
|
||||
else if (bytes < 1024 * 1024 * 1024) return (bytes / (1024 * 1024)).toFixed(2) + ' MB';
|
||||
else return (bytes / (1024 * 1024 * 1024)).toFixed(2) + ' GB';
|
||||
}
|
||||
|
||||
export {
|
||||
checkFileSizeLimit,
|
||||
checkDirectorySizeLimit,
|
||||
calculateDirectorySize,
|
||||
formatSize,
|
||||
FILE_SIZE_LIMIT,
|
||||
DIRECTORY_SIZE_LIMIT
|
||||
};
|
||||
@@ -0,0 +1,219 @@
|
||||
import axios from 'axios';
|
||||
import fs from 'fs-extra';
|
||||
import path from 'path';
|
||||
import FormData from 'form-data';
|
||||
import ora from 'ora';
|
||||
import chalk from 'chalk';
|
||||
import {
|
||||
checkFileSizeLimit,
|
||||
checkDirectorySizeLimit,
|
||||
formatSize,
|
||||
} from './uploadLimits';
|
||||
import { saveUploadHistory } from './history';
|
||||
import { getDeviceId } from './getDeviceId';
|
||||
|
||||
const ipfsApiUrl = process.env.IPFS_API_URL || 'https://ipfs.glitterprotocol.dev/api/v2';
|
||||
|
||||
interface FileInfo {
|
||||
name: string;
|
||||
path: string;
|
||||
}
|
||||
|
||||
|
||||
interface IpfsResponse {
|
||||
data: {
|
||||
data: Array<{
|
||||
Name: string;
|
||||
Hash: string;
|
||||
Size: string;
|
||||
}>;
|
||||
};
|
||||
}
|
||||
|
||||
// dist is the directory name, dirPath is all paths before dist
|
||||
let dirPath: string | null = null;
|
||||
|
||||
function loadFilesToArrRecursively(directoryPath: string, dist: string): FileInfo[] {
|
||||
const filesArr: FileInfo[] = [];
|
||||
const sep = path.sep;
|
||||
|
||||
dirPath ??= directoryPath.replace(dist, '');
|
||||
|
||||
// check if it is a directory
|
||||
if (fs.statSync(directoryPath).isDirectory()) {
|
||||
const files = fs.readdirSync(directoryPath);
|
||||
files.forEach((file) => {
|
||||
const filePath = path.join(directoryPath, file);
|
||||
if (fs.statSync(filePath).isFile()) {
|
||||
// check the file size
|
||||
const sizeCheck = checkFileSizeLimit(filePath);
|
||||
if (sizeCheck.exceeds) {
|
||||
throw new Error(`File ${file} exceeds size limit of ${formatSize(sizeCheck.limit)} (size: ${formatSize(sizeCheck.size)})`);
|
||||
}
|
||||
|
||||
const filePathWithNoEndSep = filePath.replace(dirPath!, '');
|
||||
const filePathEncodeSep = filePathWithNoEndSep.replaceAll(sep, '%2F');
|
||||
|
||||
filesArr.push({
|
||||
name: filePathEncodeSep,
|
||||
path: filePath,
|
||||
});
|
||||
} else if (fs.statSync(filePath).isDirectory()) {
|
||||
const recursiveFiles = loadFilesToArrRecursively(filePath, dist);
|
||||
filesArr.push(...recursiveFiles);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
console.error('Error: path must be a directory');
|
||||
}
|
||||
return filesArr;
|
||||
}
|
||||
|
||||
function countFilesInDirectory(directoryPath: string): number {
|
||||
let count = 0;
|
||||
const files = fs.readdirSync(directoryPath);
|
||||
|
||||
for (const file of files) {
|
||||
const filePath = path.join(directoryPath, file);
|
||||
const stats = fs.statSync(filePath);
|
||||
|
||||
if (stats.isFile()) {
|
||||
count++;
|
||||
} else if (stats.isDirectory()) {
|
||||
count += countFilesInDirectory(filePath);
|
||||
}
|
||||
}
|
||||
|
||||
return count;
|
||||
}
|
||||
|
||||
// upload directory to ipfs
|
||||
async function uploadDirectory(directoryPath: string, deviceId: string): Promise<string | null> {
|
||||
// check the size of all files in the directory
|
||||
const sizeCheck = checkDirectorySizeLimit(directoryPath);
|
||||
|
||||
// check the size limit of single file
|
||||
if (sizeCheck.exceeds) {
|
||||
throw new Error(`Directory ${directoryPath} exceeds size limit of ${formatSize(sizeCheck.limit)} (size: ${formatSize(sizeCheck.size)})`);
|
||||
}
|
||||
|
||||
const formData = new FormData();
|
||||
|
||||
// redundant check for directoryPath, ensure directoryPath ends with no separator
|
||||
if (directoryPath.endsWith(path.sep)) directoryPath = directoryPath.slice(0, -1);
|
||||
|
||||
// get the last layer directory, as the ipfs directory name
|
||||
const dist = directoryPath.split(path.sep).pop() || '';
|
||||
|
||||
// recursively get all files
|
||||
const files = loadFilesToArrRecursively(directoryPath, dist);
|
||||
files.forEach((file) => {
|
||||
formData.append('file', fs.createReadStream(file.path), {
|
||||
filename: file.name,
|
||||
});
|
||||
});
|
||||
formData.append('uid', deviceId);
|
||||
|
||||
const spinner = ora(`Uploading ${directoryPath} to glitter ipfs...`).start();
|
||||
const response = await axios.post<IpfsResponse['data']>(`${ipfsApiUrl}/add`, formData, {
|
||||
headers: {
|
||||
...formData.getHeaders(),
|
||||
},
|
||||
});
|
||||
|
||||
const resData = response.data.data;
|
||||
// check if the returned data is an array and contains at least one element
|
||||
if (Array.isArray(resData) && resData.length > 0) {
|
||||
spinner.succeed();
|
||||
// find the object with Name as an empty string, get the directory hash
|
||||
const directoryItem = resData.find((item) => item.Name === dist);
|
||||
if (directoryItem) {
|
||||
const fileStats = fs.statSync(directoryPath);
|
||||
const fileCount = countFilesInDirectory(directoryPath);
|
||||
const uploadData = {
|
||||
path: directoryPath,
|
||||
filename: path.basename(directoryPath),
|
||||
contentHash: directoryItem.Hash,
|
||||
previewHash: null,
|
||||
size: sizeCheck.size,
|
||||
fileCount: fileCount,
|
||||
isDirectory: true
|
||||
};
|
||||
saveUploadHistory(uploadData);
|
||||
return directoryItem.Hash;
|
||||
}
|
||||
spinner.fail();
|
||||
console.log(chalk.red(`Directory hash not found in response`));
|
||||
} else {
|
||||
spinner.fail();
|
||||
console.log(chalk.red(`Invalid response format from IPFS`));
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// upload file to ipfs
|
||||
async function uploadFile(filePath: string, deviceId: string): Promise<string | null> {
|
||||
const sizeCheck = checkFileSizeLimit(filePath);
|
||||
if (sizeCheck.exceeds) {
|
||||
throw new Error(`File ${filePath} exceeds size limit of ${formatSize(sizeCheck.limit)} (size: ${formatSize(sizeCheck.size)})`);
|
||||
}
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append('file', fs.createReadStream(filePath), {
|
||||
filename: filePath.split(path.sep).pop() || '',
|
||||
});
|
||||
formData.append('uid', deviceId);
|
||||
|
||||
const spinner = ora(`Uploading ${filePath} to glitter ipfs...`).start();
|
||||
const response = await axios.post<IpfsResponse['data']>(`${ipfsApiUrl}/add`, formData, {
|
||||
headers: {
|
||||
...formData.getHeaders(),
|
||||
},
|
||||
});
|
||||
|
||||
const resData = response.data.data;
|
||||
// check if the returned data is an array and contains at least one element
|
||||
if (Array.isArray(resData) && resData.length > 0) {
|
||||
spinner.succeed();
|
||||
// find the object with Name as an empty string, get the file hash
|
||||
const fileItem = resData.find((item) => item.Name === filePath.split(path.sep).pop() || '');
|
||||
if (fileItem) {
|
||||
const uploadData = {
|
||||
path: filePath,
|
||||
filename: filePath.split(path.sep).pop() || '',
|
||||
contentHash: fileItem.Hash,
|
||||
previewHash: null,
|
||||
size: sizeCheck.size,
|
||||
fileCount: 1,
|
||||
isDirectory: false
|
||||
};
|
||||
saveUploadHistory(uploadData);
|
||||
return fileItem.Hash;
|
||||
}
|
||||
spinner.fail();
|
||||
console.log(chalk.red(`File hash not found in response`));
|
||||
} else {
|
||||
spinner.fail();
|
||||
console.log(chalk.red(`Invalid response format from IPFS`));
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export default async function(filePath: string): Promise<{contentHash: string, previewHash?: string | null}> {
|
||||
// check if the file is a directory
|
||||
const deviceId = getDeviceId();
|
||||
if (!deviceId) {
|
||||
throw new Error('Device ID not found');
|
||||
}
|
||||
if (fs.statSync(filePath).isDirectory()) {
|
||||
return {
|
||||
contentHash: await uploadDirectory(filePath, deviceId) || '',
|
||||
previewHash: null
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
contentHash: await uploadFile(filePath, deviceId) || '',
|
||||
previewHash: null
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
require('dotenv').config();
|
||||
const esbuild = require('esbuild');
|
||||
|
||||
const define = {};
|
||||
for (const key in process.env) {
|
||||
define[`process.env.${key}`] = JSON.stringify(process.env[key]);
|
||||
}
|
||||
|
||||
define['process.env.IPFS_PREVIEW_URL'] = JSON.stringify(process.env.IPFS_PREVIEW_URL);
|
||||
define['process.env.SECRET_KEY'] = JSON.stringify(process.env.SECRET_KEY);
|
||||
|
||||
esbuild.build({
|
||||
entryPoints: ['bin/index.ts'],
|
||||
outfile: 'dist/index.js',
|
||||
bundle: true,
|
||||
platform: 'node',
|
||||
target: 'node14',
|
||||
format: 'cjs',
|
||||
external: Object.keys(require('./package.json').dependencies || {}),
|
||||
banner: { js: '#!/usr/bin/env node' },
|
||||
logLevel: 'info',
|
||||
define,
|
||||
}).catch(() => process.exit(1));
|
||||
BIN
Binary file not shown.
@@ -0,0 +1,4 @@
|
||||
sh build.sh
|
||||
blog run serve & pinme upload ./dist
|
||||
|
||||
ghost
|
||||
@@ -0,0 +1 @@
|
||||
docsify
|
||||
@@ -0,0 +1,63 @@
|
||||
{
|
||||
"name": "pinme",
|
||||
"version": "1.0.1",
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
"description": "Deploy Your Frontend In a Single Command",
|
||||
"main": "dist/index.js",
|
||||
"scripts": {
|
||||
"build": "node build.js",
|
||||
"dev": "NODE_ENV=development node build.js",
|
||||
"prepublishOnly": "npm run build"
|
||||
},
|
||||
"bin": {
|
||||
"pinme": "./dist/index.js"
|
||||
},
|
||||
"files": [
|
||||
"dist"
|
||||
],
|
||||
"keywords": [
|
||||
"ipfs",
|
||||
"cli",
|
||||
"deploy",
|
||||
"frontend"
|
||||
],
|
||||
"author": "Glitter Protocol",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"axios": "^1.3.2",
|
||||
"base-x": "^5.0.1",
|
||||
"bip39": "^3.1.0",
|
||||
"chalk": "^2.4.2",
|
||||
"commander": "^11.1.0",
|
||||
"crypto-js": "^4.2.0",
|
||||
"dayjs": "^1.11.7",
|
||||
"ethers": "5.7.2",
|
||||
"figlet": "^1.7.0",
|
||||
"form-data": "^4.0.0",
|
||||
"fs-extra": "^11.2.0",
|
||||
"inquirer": "^8.2.5",
|
||||
"ora": "^3.2.0",
|
||||
"uuid": "^9.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@rollup/plugin-commonjs": "^22.0.2",
|
||||
"@rollup/plugin-json": "^4.1.0",
|
||||
"@rollup/plugin-node-resolve": "^14.1.0",
|
||||
"dotenv": "^16.5.0",
|
||||
"esbuild": "^0.25.2",
|
||||
"eslint": "^8.33.0",
|
||||
"eslint-config-airbnb-base": "^15.0.0",
|
||||
"eslint-config-prettier": "^8.6.0",
|
||||
"eslint-plugin-import": "^2.27.5",
|
||||
"eslint-plugin-prettier": "^4.2.1",
|
||||
"prettier": "^2.8.3",
|
||||
"rollup": "^2.79.1",
|
||||
"rollup-plugin-copy": "^3.5.0",
|
||||
"rollup-plugin-terser": "^7.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 14.18.0"
|
||||
}
|
||||
}
|
||||
Generated
+3330
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,73 @@
|
||||
const { nodeResolve } = require('@rollup/plugin-node-resolve');
|
||||
const commonjs = require('@rollup/plugin-commonjs');
|
||||
const json = require('@rollup/plugin-json');
|
||||
const typescript = require('@rollup/plugin-typescript');
|
||||
const { terser } = require('rollup-plugin-terser');
|
||||
const copy = require('rollup-plugin-copy');
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
|
||||
// 确保dist目录存在
|
||||
const distDir = path.resolve(__dirname, 'dist');
|
||||
if (!fs.existsSync(distDir)) {
|
||||
fs.mkdirSync(distDir, { recursive: true });
|
||||
}
|
||||
|
||||
|
||||
module.exports = {
|
||||
input: 'bin/index.ts',
|
||||
output: {
|
||||
file: 'dist/index.js',
|
||||
format: 'cjs',
|
||||
banner: '#!/usr/bin/env node',
|
||||
sourcemap: false,
|
||||
},
|
||||
// 将所有npm依赖项设为外部依赖,不打包进最终文件
|
||||
external: [
|
||||
...Object.keys(require('./package.json').dependencies || {}),
|
||||
...require('module').builtinModules,
|
||||
],
|
||||
plugins: [
|
||||
typescript({
|
||||
tsconfig: './tsconfig.json',
|
||||
sourceMap: false
|
||||
}),
|
||||
// 解析第三方模块
|
||||
nodeResolve({
|
||||
preferBuiltins: true,
|
||||
}),
|
||||
// 转换CommonJS模块为ES模块
|
||||
commonjs(),
|
||||
// 支持导入JSON文件
|
||||
json(),
|
||||
// 压缩代码
|
||||
terser({
|
||||
format: {
|
||||
comments: false,
|
||||
},
|
||||
compress: {
|
||||
drop_console: false,
|
||||
drop_debugger: true,
|
||||
},
|
||||
}),
|
||||
// 自定义插件:确保生成的文件有执行权限
|
||||
{
|
||||
name: 'make-executable',
|
||||
writeBundle() {
|
||||
const outputFile = path.resolve(__dirname, 'dist/index.js');
|
||||
try {
|
||||
fs.chmodSync(outputFile, '755');
|
||||
console.log('Added executable permissions to output file');
|
||||
} catch (error) {
|
||||
console.error('Failed to add executable permissions:', error);
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
onwarn(warning, warn) {
|
||||
// 忽略某些警告
|
||||
if (warning.code === 'CIRCULAR_DEPENDENCY') return;
|
||||
if (warning.code === 'UNRESOLVED_IMPORT' && warning.source.startsWith('node:')) return;
|
||||
warn(warning);
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ESNext",
|
||||
"lib": ["ESNext"],
|
||||
"skipLibCheck": true,
|
||||
"esModuleInterop": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"strict": false,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "Node",
|
||||
"resolveJsonModule": true,
|
||||
"types": ["node"],
|
||||
"outDir": "dist",
|
||||
"declaration": true,
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["src/*"]
|
||||
}
|
||||
},
|
||||
"include": ["bin/**/*"],
|
||||
"exclude": ["node_modules", "dist"],
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "Node",
|
||||
"allowSyntheticDefaultImports": true
|
||||
},
|
||||
"include": ["vite.config.ts"]
|
||||
}
|
||||
Reference in New Issue
Block a user