mirror of
https://github.com/glitternetwork/pinme.git
synced 2026-09-01 15:33:43 +08:00
Merge pull request #49 from glitternetwork/fix-check_status
Fix check status
This commit is contained in:
+2
-1
@@ -7,4 +7,5 @@ node_modules
|
||||
/test-repro
|
||||
.catpaw
|
||||
.claude/settings.local.json
|
||||
chrome-test-1
|
||||
chrome-test-1
|
||||
docs
|
||||
+53
@@ -17,6 +17,15 @@ import {
|
||||
validateDnsDomain,
|
||||
} from './utils/domainValidator';
|
||||
import { uploadPath } from './services/uploadService';
|
||||
import tracker, {
|
||||
getPathKind,
|
||||
getTrackErrorReason,
|
||||
} from './utils/tracker';
|
||||
import {
|
||||
TRACK_EVENTS,
|
||||
TRACK_PAGES,
|
||||
resolveTrackAction,
|
||||
} from './utils/trackerEvents';
|
||||
|
||||
interface Args {
|
||||
domain?: string;
|
||||
@@ -114,6 +123,8 @@ export default async function bindCmd(): Promise<void> {
|
||||
// Auto-detect domain type if not explicitly specified
|
||||
const isDns = dns || isDnsDomain(domain);
|
||||
const displayDomain = normalizeDomain(domain);
|
||||
const pathKind = getPathKind(path.resolve(targetPath));
|
||||
const domainType = isDns ? 'dns' : 'pinme_subdomain';
|
||||
|
||||
// Validate DNS domain format
|
||||
if (isDns) {
|
||||
@@ -165,9 +176,20 @@ export default async function bindCmd(): Promise<void> {
|
||||
console.log(chalk.blue(`Uploading: ${absolutePath}`));
|
||||
const up = await uploadPath(absolutePath, { uid: authConfig.address });
|
||||
if (!up?.contentHash) {
|
||||
void tracker.trackEvent(TRACK_EVENTS.uploadFailed, TRACK_PAGES.upload, {
|
||||
a: resolveTrackAction(TRACK_EVENTS.uploadFailed),
|
||||
path_kind: pathKind,
|
||||
has_domain: true,
|
||||
reason: 'no_content_hash',
|
||||
});
|
||||
console.log(chalk.red('Upload failed, binding aborted.'));
|
||||
return;
|
||||
}
|
||||
void tracker.trackEvent(TRACK_EVENTS.uploadSuccess, TRACK_PAGES.upload, {
|
||||
a: resolveTrackAction(TRACK_EVENTS.uploadSuccess),
|
||||
path_kind: pathKind,
|
||||
has_domain: true,
|
||||
});
|
||||
console.log(chalk.green(`Upload success, CID: ${up.contentHash}`));
|
||||
|
||||
// Bind domain
|
||||
@@ -182,9 +204,22 @@ export default async function bindCmd(): Promise<void> {
|
||||
authConfig.token,
|
||||
);
|
||||
if (dnsResult.code !== 200) {
|
||||
void tracker.trackEvent(TRACK_EVENTS.domainBindFailed, TRACK_PAGES.domain, {
|
||||
a: resolveTrackAction(TRACK_EVENTS.domainBindFailed),
|
||||
domain_type: domainType,
|
||||
domain_name: displayDomain,
|
||||
bind_source: 'bind',
|
||||
reason: dnsResult.msg || 'dns_bind_failed',
|
||||
});
|
||||
console.log(chalk.red(`DNS binding failed: ${dnsResult.msg}`));
|
||||
return;
|
||||
}
|
||||
void tracker.trackEvent(TRACK_EVENTS.domainBindSuccess, TRACK_PAGES.domain, {
|
||||
a: resolveTrackAction(TRACK_EVENTS.domainBindSuccess),
|
||||
domain_type: domainType,
|
||||
domain_name: displayDomain,
|
||||
bind_source: 'bind',
|
||||
});
|
||||
console.log(chalk.green(`DNS bind success: ${displayDomain}`));
|
||||
console.log(chalk.white(`Visit: https://${displayDomain}`));
|
||||
console.log(
|
||||
@@ -197,9 +232,22 @@ export default async function bindCmd(): Promise<void> {
|
||||
console.log(chalk.blue('Binding Pinme subdomain...'));
|
||||
const ok = await bindPinmeDomain(displayDomain, up.contentHash);
|
||||
if (!ok) {
|
||||
void tracker.trackEvent(TRACK_EVENTS.domainBindFailed, TRACK_PAGES.domain, {
|
||||
a: resolveTrackAction(TRACK_EVENTS.domainBindFailed),
|
||||
domain_type: domainType,
|
||||
domain_name: displayDomain,
|
||||
bind_source: 'bind',
|
||||
reason: 'pinme_bind_failed',
|
||||
});
|
||||
console.log(chalk.red('Binding failed. Please try again later.'));
|
||||
return;
|
||||
}
|
||||
void tracker.trackEvent(TRACK_EVENTS.domainBindSuccess, TRACK_PAGES.domain, {
|
||||
a: resolveTrackAction(TRACK_EVENTS.domainBindSuccess),
|
||||
domain_type: domainType,
|
||||
domain_name: displayDomain,
|
||||
bind_source: 'bind',
|
||||
});
|
||||
console.log(chalk.green(`Bind success: ${displayDomain}`));
|
||||
const rootDomain = await getRootDomain();
|
||||
console.log(
|
||||
@@ -213,6 +261,11 @@ export default async function bindCmd(): Promise<void> {
|
||||
throw e;
|
||||
}
|
||||
} catch (e: any) {
|
||||
void tracker.trackEvent(TRACK_EVENTS.domainBindFailed, TRACK_PAGES.domain, {
|
||||
a: resolveTrackAction(TRACK_EVENTS.domainBindFailed),
|
||||
bind_source: 'bind',
|
||||
reason: getTrackErrorReason(e),
|
||||
});
|
||||
printCliError(e, 'Bind failed.');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,6 +15,12 @@ import {
|
||||
import { APP_CONFIG, getPinmeApiUrl } from './utils/config';
|
||||
import { uploadPath } from './services/uploadService';
|
||||
import { printHighlightedUrl } from './utils/urlDisplay';
|
||||
import tracker, { getTrackErrorReason } from './utils/tracker';
|
||||
import {
|
||||
TRACK_EVENTS,
|
||||
TRACK_PAGES,
|
||||
resolveTrackAction,
|
||||
} from './utils/trackerEvents';
|
||||
|
||||
// Template directory - relative to bin folder (works both in dev and npm)
|
||||
const PROJECT_DIR = process.cwd();
|
||||
@@ -590,8 +596,30 @@ export default async function createCmd(options: CreateOptions): Promise<void> {
|
||||
console.log(chalk.gray(` cd ${projectName}`));
|
||||
console.log(chalk.gray(` pinme save`));
|
||||
|
||||
void tracker.trackEvent(
|
||||
TRACK_EVENTS.projectCreateSuccess,
|
||||
TRACK_PAGES.project,
|
||||
{
|
||||
a: resolveTrackAction(TRACK_EVENTS.projectCreateSuccess),
|
||||
project_name: workerData.project_name,
|
||||
template_branch: TEMPLATE_BRANCH,
|
||||
force: Boolean(options.force),
|
||||
},
|
||||
);
|
||||
|
||||
process.exit(0);
|
||||
} catch (error: any) {
|
||||
void tracker.trackEvent(
|
||||
TRACK_EVENTS.projectCreateFailed,
|
||||
TRACK_PAGES.project,
|
||||
{
|
||||
a: resolveTrackAction(TRACK_EVENTS.projectCreateFailed),
|
||||
project_name: options.name,
|
||||
template_branch: TEMPLATE_BRANCH,
|
||||
force: Boolean(options.force),
|
||||
reason: getTrackErrorReason(error),
|
||||
},
|
||||
);
|
||||
printCliError(error, 'Project creation failed.');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
@@ -5,6 +5,12 @@ import fs from 'fs-extra';
|
||||
import path from 'path';
|
||||
import { getAuthHeaders } from './utils/webLogin';
|
||||
import { getPinmeApiUrl } from './utils/config';
|
||||
import tracker, { getTrackErrorReason } from './utils/tracker';
|
||||
import {
|
||||
TRACK_EVENTS,
|
||||
TRACK_PAGES,
|
||||
resolveTrackAction,
|
||||
} from './utils/trackerEvents';
|
||||
|
||||
interface DeleteOptions {
|
||||
name?: string;
|
||||
@@ -95,6 +101,17 @@ export default async function deleteCmd(options: DeleteOptions): Promise<void> {
|
||||
const data = response.data;
|
||||
|
||||
if (data.code === 200) {
|
||||
void tracker.trackEvent(
|
||||
TRACK_EVENTS.projectDeleteSuccess,
|
||||
TRACK_PAGES.project,
|
||||
{
|
||||
a: resolveTrackAction(TRACK_EVENTS.projectDeleteSuccess),
|
||||
project_name: data.data.project_name,
|
||||
domain_deleted: Boolean(data.data.domain_deleted),
|
||||
worker_deleted: Boolean(data.data.worker_deleted),
|
||||
database_deleted: Boolean(data.data.database_deleted),
|
||||
},
|
||||
);
|
||||
console.log(chalk.green('\n✅ Project deleted successfully!'));
|
||||
console.log(chalk.gray(`\nProject: ${data.data.project_name}`));
|
||||
console.log(chalk.gray(` Domain deleted: ${data.data.domain_deleted ? '✅' : '❌'}`));
|
||||
@@ -109,6 +126,16 @@ export default async function deleteCmd(options: DeleteOptions): Promise<void> {
|
||||
|
||||
process.exit(0);
|
||||
} catch (error: any) {
|
||||
void tracker.trackEvent(
|
||||
TRACK_EVENTS.projectDeleteFailed,
|
||||
TRACK_PAGES.project,
|
||||
{
|
||||
a: resolveTrackAction(TRACK_EVENTS.projectDeleteFailed),
|
||||
project_name: options.name || getProjectName() || undefined,
|
||||
force: Boolean(options.force),
|
||||
reason: getTrackErrorReason(error),
|
||||
},
|
||||
);
|
||||
console.log(chalk.red(error));
|
||||
const errorMsg = error.response?.data?.msg
|
||||
|| error.message
|
||||
|
||||
@@ -10,6 +10,12 @@ import { printCliError } from './utils/cliError';
|
||||
import { requestCarExport, checkCarExportStatus } from './utils/pinmeApi';
|
||||
import { getUid } from './utils/getDeviceId';
|
||||
import { checkNodeVersion } from './utils/checkNodeVersion';
|
||||
import tracker, { getTrackErrorReason } from './utils/tracker';
|
||||
import {
|
||||
TRACK_EVENTS,
|
||||
TRACK_PAGES,
|
||||
resolveTrackAction,
|
||||
} from './utils/trackerEvents';
|
||||
|
||||
checkNodeVersion();
|
||||
|
||||
@@ -235,6 +241,11 @@ export default async (): Promise<void> => {
|
||||
);
|
||||
|
||||
if (!downloadUrl) {
|
||||
void tracker.trackEvent(TRACK_EVENTS.exportFailed, TRACK_PAGES.export, {
|
||||
a: resolveTrackAction(TRACK_EVENTS.exportFailed),
|
||||
cid,
|
||||
reason: 'export_failed_or_timed_out',
|
||||
});
|
||||
console.log(chalk.red('Export failed or timed out.'));
|
||||
return;
|
||||
}
|
||||
@@ -242,6 +253,11 @@ export default async (): Promise<void> => {
|
||||
// Step 3: Download CAR file
|
||||
const success = await downloadCarFile(downloadUrl, finalOutputPath);
|
||||
if (success) {
|
||||
void tracker.trackEvent(TRACK_EVENTS.exportSuccess, TRACK_PAGES.export, {
|
||||
a: resolveTrackAction(TRACK_EVENTS.exportSuccess),
|
||||
cid,
|
||||
output_dir: outputDir,
|
||||
});
|
||||
const fileSize = fs.statSync(finalOutputPath).size;
|
||||
const fileSizeMB = (fileSize / (1024 * 1024)).toFixed(2);
|
||||
console.log(
|
||||
@@ -254,13 +270,27 @@ export default async (): Promise<void> => {
|
||||
console.log(chalk.cyan(`Size: ${fileSizeMB} MB`));
|
||||
console.log(chalk.cyan(`CID: ${cid}`));
|
||||
} else {
|
||||
void tracker.trackEvent(TRACK_EVENTS.exportFailed, TRACK_PAGES.export, {
|
||||
a: resolveTrackAction(TRACK_EVENTS.exportFailed),
|
||||
cid,
|
||||
reason: 'download_failed',
|
||||
});
|
||||
console.log(chalk.red('Download failed.'));
|
||||
}
|
||||
} catch (error: any) {
|
||||
void tracker.trackEvent(TRACK_EVENTS.exportFailed, TRACK_PAGES.export, {
|
||||
a: resolveTrackAction(TRACK_EVENTS.exportFailed),
|
||||
cid,
|
||||
reason: getTrackErrorReason(error),
|
||||
});
|
||||
spinner.fail(`Error: ${error.message}`);
|
||||
printCliError(error, 'Export failed.');
|
||||
}
|
||||
} catch (error: any) {
|
||||
void tracker.trackEvent(TRACK_EVENTS.exportFailed, TRACK_PAGES.export, {
|
||||
a: resolveTrackAction(TRACK_EVENTS.exportFailed),
|
||||
reason: getTrackErrorReason(error),
|
||||
});
|
||||
printCliError(error, 'Export failed.');
|
||||
}
|
||||
};
|
||||
|
||||
@@ -14,6 +14,15 @@ import { getAuthConfig } from './utils/webLogin';
|
||||
import { APP_CONFIG } from './utils/config';
|
||||
import { uploadPath } from './services/uploadService';
|
||||
import { printHighlightedUrl } from './utils/urlDisplay';
|
||||
import tracker, {
|
||||
getPathKind,
|
||||
getTrackErrorReason,
|
||||
} from './utils/tracker';
|
||||
import {
|
||||
TRACK_EVENTS,
|
||||
TRACK_PAGES,
|
||||
resolveTrackAction,
|
||||
} from './utils/trackerEvents';
|
||||
// get from environment variables
|
||||
|
||||
import { checkNodeVersion } from './utils/checkNodeVersion';
|
||||
@@ -111,6 +120,7 @@ export default async (options?: ImportOptions): Promise<void> => {
|
||||
console.log(chalk.red(`path ${argPath} does not exist`));
|
||||
return;
|
||||
}
|
||||
const pathKind = getPathKind(absolutePath);
|
||||
|
||||
// optional: pre-check domain availability before import
|
||||
if (domainArg) {
|
||||
@@ -133,6 +143,11 @@ export default async (options?: ImportOptions): Promise<void> => {
|
||||
uid: getUid(),
|
||||
});
|
||||
if (result) {
|
||||
void tracker.trackEvent(TRACK_EVENTS.importSuccess, TRACK_PAGES.import, {
|
||||
a: resolveTrackAction(TRACK_EVENTS.importSuccess),
|
||||
path_kind: pathKind,
|
||||
has_domain: Boolean(domainArg),
|
||||
});
|
||||
const uid = getUid();
|
||||
const encryptedCID = encryptHash(
|
||||
result.contentHash,
|
||||
@@ -158,6 +173,12 @@ export default async (options?: ImportOptions): Promise<void> => {
|
||||
);
|
||||
const ok = await bindPinmeDomain(domainArg, result.contentHash);
|
||||
if (ok) {
|
||||
void tracker.trackEvent(TRACK_EVENTS.domainBindSuccess, TRACK_PAGES.domain, {
|
||||
a: resolveTrackAction(TRACK_EVENTS.domainBindSuccess),
|
||||
domain_type: 'pinme_subdomain',
|
||||
domain_name: domainArg,
|
||||
bind_source: 'import',
|
||||
});
|
||||
console.log(chalk.green(`Bind success: ${domainArg}`));
|
||||
const rootDomain = await getRootDomain();
|
||||
console.log(
|
||||
@@ -166,12 +187,25 @@ export default async (options?: ImportOptions): Promise<void> => {
|
||||
),
|
||||
);
|
||||
} else {
|
||||
void tracker.trackEvent(TRACK_EVENTS.domainBindFailed, TRACK_PAGES.domain, {
|
||||
a: resolveTrackAction(TRACK_EVENTS.domainBindFailed),
|
||||
domain_type: 'pinme_subdomain',
|
||||
domain_name: domainArg,
|
||||
bind_source: 'import',
|
||||
reason: 'pinme_bind_failed',
|
||||
});
|
||||
console.log(chalk.red('Binding failed. Please try again later.'));
|
||||
}
|
||||
}
|
||||
console.log(chalk.green('\n🎉 import successful, program exit'));
|
||||
}
|
||||
} catch (error: any) {
|
||||
void tracker.trackEvent(TRACK_EVENTS.importFailed, TRACK_PAGES.import, {
|
||||
a: resolveTrackAction(TRACK_EVENTS.importFailed),
|
||||
path_kind: pathKind,
|
||||
has_domain: Boolean(domainArg),
|
||||
reason: getTrackErrorReason(error),
|
||||
});
|
||||
printCliError(error, 'Import failed.');
|
||||
}
|
||||
process.exit(0);
|
||||
@@ -192,6 +226,7 @@ export default async (options?: ImportOptions): Promise<void> => {
|
||||
console.log(chalk.red(`path ${answer.path} does not exist`));
|
||||
return;
|
||||
}
|
||||
const pathKind = getPathKind(absolutePath);
|
||||
|
||||
// optional: interactive flow may also parse --domain, reuse the same arg parsing
|
||||
if (domainArg) {
|
||||
@@ -215,6 +250,11 @@ export default async (options?: ImportOptions): Promise<void> => {
|
||||
});
|
||||
|
||||
if (result) {
|
||||
void tracker.trackEvent(TRACK_EVENTS.importSuccess, TRACK_PAGES.import, {
|
||||
a: resolveTrackAction(TRACK_EVENTS.importSuccess),
|
||||
path_kind: pathKind,
|
||||
has_domain: Boolean(domainArg),
|
||||
});
|
||||
const uid = getUid();
|
||||
const encryptedCID = encryptHash(
|
||||
result.contentHash,
|
||||
@@ -239,6 +279,12 @@ export default async (options?: ImportOptions): Promise<void> => {
|
||||
);
|
||||
const ok = await bindPinmeDomain(domainArg, result.contentHash);
|
||||
if (ok) {
|
||||
void tracker.trackEvent(TRACK_EVENTS.domainBindSuccess, TRACK_PAGES.domain, {
|
||||
a: resolveTrackAction(TRACK_EVENTS.domainBindSuccess),
|
||||
domain_type: 'pinme_subdomain',
|
||||
domain_name: domainArg,
|
||||
bind_source: 'import',
|
||||
});
|
||||
console.log(chalk.green(`Bind success: ${domainArg}`));
|
||||
const rootDomain = await getRootDomain();
|
||||
console.log(
|
||||
@@ -247,17 +293,34 @@ export default async (options?: ImportOptions): Promise<void> => {
|
||||
),
|
||||
);
|
||||
} else {
|
||||
void tracker.trackEvent(TRACK_EVENTS.domainBindFailed, TRACK_PAGES.domain, {
|
||||
a: resolveTrackAction(TRACK_EVENTS.domainBindFailed),
|
||||
domain_type: 'pinme_subdomain',
|
||||
domain_name: domainArg,
|
||||
bind_source: 'import',
|
||||
reason: 'pinme_bind_failed',
|
||||
});
|
||||
console.log(chalk.red('Binding failed. Please try again later.'));
|
||||
}
|
||||
}
|
||||
console.log(chalk.green('\n🎉 import successful, program exit'));
|
||||
}
|
||||
} catch (error: any) {
|
||||
void tracker.trackEvent(TRACK_EVENTS.importFailed, TRACK_PAGES.import, {
|
||||
a: resolveTrackAction(TRACK_EVENTS.importFailed),
|
||||
path_kind: pathKind,
|
||||
has_domain: Boolean(domainArg),
|
||||
reason: getTrackErrorReason(error),
|
||||
});
|
||||
printCliError(error, 'Import failed.');
|
||||
}
|
||||
process.exit(0);
|
||||
}
|
||||
} catch (error: any) {
|
||||
void tracker.trackEvent(TRACK_EVENTS.importFailed, TRACK_PAGES.import, {
|
||||
a: resolveTrackAction(TRACK_EVENTS.importFailed),
|
||||
reason: getTrackErrorReason(error),
|
||||
});
|
||||
printCliError(error, 'Import failed.');
|
||||
}
|
||||
};
|
||||
|
||||
+20
-1
@@ -2,6 +2,12 @@ import chalk from 'chalk';
|
||||
import { WebLoginManager } from './utils/webLogin';
|
||||
import { getDeviceId } from './utils/getDeviceId';
|
||||
import { bindAnonymousDevice } from './utils/pinmeApi';
|
||||
import tracker, { getTrackErrorReason } from './utils/tracker';
|
||||
import {
|
||||
TRACK_EVENTS,
|
||||
TRACK_PAGES,
|
||||
resolveTrackAction,
|
||||
} from './utils/trackerEvents';
|
||||
|
||||
export interface EnvOption {
|
||||
env?: string;
|
||||
@@ -14,11 +20,12 @@ const ENV_URLS: Record<string, string> = {
|
||||
};
|
||||
|
||||
export default async function loginCmd(options: EnvOption = {}): Promise<void> {
|
||||
const env = (options.env || 'prod').toLowerCase();
|
||||
|
||||
try {
|
||||
// Determine web base URL based on env
|
||||
let webBaseUrl: string | undefined;
|
||||
// 默认使用 prod 环境
|
||||
const env = (options.env || 'prod').toLowerCase();
|
||||
if (ENV_URLS[env]) {
|
||||
webBaseUrl = ENV_URLS[env];
|
||||
console.log(chalk.blue(`Using ${env} environment: ${webBaseUrl}`));
|
||||
@@ -44,9 +51,21 @@ export default async function loginCmd(options: EnvOption = {}): Promise<void> {
|
||||
console.log(chalk.green('History merged to your account'));
|
||||
}
|
||||
|
||||
void tracker.trackEvent(TRACK_EVENTS.cliLoginSuccess, TRACK_PAGES.login, {
|
||||
a: resolveTrackAction(TRACK_EVENTS.cliLoginSuccess),
|
||||
env,
|
||||
has_token_address: true,
|
||||
merged_anonymous_history: ok,
|
||||
});
|
||||
|
||||
// Exit the process after successful login
|
||||
process.exit(0);
|
||||
} catch (e: any) {
|
||||
void tracker.trackEvent(TRACK_EVENTS.cliLoginFailed, TRACK_PAGES.login, {
|
||||
a: resolveTrackAction(TRACK_EVENTS.cliLoginFailed),
|
||||
env,
|
||||
reason: getTrackErrorReason(e),
|
||||
});
|
||||
console.log(chalk.red(`\nLogin failed: ${e?.message || e}`));
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,12 @@
|
||||
import chalk from 'chalk';
|
||||
import inquirer from 'inquirer';
|
||||
import { clearAuthToken, getAuthConfig } from './utils/webLogin';
|
||||
import tracker, { getTrackErrorReason } from './utils/tracker';
|
||||
import {
|
||||
TRACK_EVENTS,
|
||||
TRACK_PAGES,
|
||||
resolveTrackAction,
|
||||
} from './utils/trackerEvents';
|
||||
|
||||
export default async function logoutCmd(): Promise<void> {
|
||||
try {
|
||||
@@ -28,9 +34,17 @@ export default async function logoutCmd(): Promise<void> {
|
||||
|
||||
// Clear auth token
|
||||
clearAuthToken();
|
||||
void tracker.trackEvent(TRACK_EVENTS.logoutSuccess, TRACK_PAGES.auth, {
|
||||
a: resolveTrackAction(TRACK_EVENTS.logoutSuccess),
|
||||
had_session: true,
|
||||
});
|
||||
console.log(chalk.green('Successfully logged out.'));
|
||||
console.log(chalk.gray(`Address ${auth.address} has been removed from local storage.`));
|
||||
} catch (e: any) {
|
||||
void tracker.trackEvent(TRACK_EVENTS.logoutFailed, TRACK_PAGES.auth, {
|
||||
a: resolveTrackAction(TRACK_EVENTS.logoutFailed),
|
||||
reason: getTrackErrorReason(e),
|
||||
});
|
||||
console.log(chalk.red(`Failed to logout: ${e?.message || e}`));
|
||||
}
|
||||
}
|
||||
|
||||
+14
-1
@@ -2,10 +2,20 @@ import chalk from 'chalk';
|
||||
import dayjs from 'dayjs';
|
||||
import { printCliError } from './utils/cliError';
|
||||
import { getMyDomains } from './utils/pinmeApi';
|
||||
import tracker, { getTrackErrorReason } from './utils/tracker';
|
||||
import {
|
||||
TRACK_EVENTS,
|
||||
TRACK_PAGES,
|
||||
resolveTrackAction,
|
||||
} from './utils/trackerEvents';
|
||||
|
||||
export default async function myDomainsCmd(): Promise<void> {
|
||||
try {
|
||||
const list = await getMyDomains();
|
||||
void tracker.trackEvent(TRACK_EVENTS.myDomainsSuccess, TRACK_PAGES.domain, {
|
||||
a: resolveTrackAction(TRACK_EVENTS.myDomainsSuccess),
|
||||
domain_count: list.length,
|
||||
});
|
||||
if (!list.length) {
|
||||
console.log(chalk.yellow('No bound domains found.'));
|
||||
return;
|
||||
@@ -26,7 +36,10 @@ export default async function myDomainsCmd(): Promise<void> {
|
||||
console.log(chalk.cyan('-'.repeat(80)));
|
||||
});
|
||||
} catch (e: any) {
|
||||
void tracker.trackEvent(TRACK_EVENTS.myDomainsFailed, TRACK_PAGES.domain, {
|
||||
a: resolveTrackAction(TRACK_EVENTS.myDomainsFailed),
|
||||
reason: getTrackErrorReason(e),
|
||||
});
|
||||
printCliError(e, 'Failed to fetch domains.');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,12 @@ import chalk from 'chalk';
|
||||
import inquirer from 'inquirer';
|
||||
import figlet from 'figlet';
|
||||
import { removeFromIpfs } from './utils/removeFromIpfs';
|
||||
import tracker, { getTrackErrorReason } from './utils/tracker';
|
||||
import {
|
||||
TRACK_EVENTS,
|
||||
TRACK_PAGES,
|
||||
resolveTrackAction,
|
||||
} from './utils/trackerEvents';
|
||||
|
||||
interface RemoveOptions {
|
||||
[key: string]: any;
|
||||
@@ -97,6 +103,10 @@ export default async (options?: RemoveOptions): Promise<void> => {
|
||||
try {
|
||||
const success = await removeFromIpfs(parsedInput.value, parsedInput.type);
|
||||
if (success) {
|
||||
void tracker.trackEvent(TRACK_EVENTS.removeSuccess, TRACK_PAGES.remove, {
|
||||
a: resolveTrackAction(TRACK_EVENTS.removeSuccess),
|
||||
input_type: parsedInput.type,
|
||||
});
|
||||
console.log(
|
||||
chalk.cyan(
|
||||
figlet.textSync('Successful', { horizontalLayout: 'full' }),
|
||||
@@ -104,6 +114,11 @@ export default async (options?: RemoveOptions): Promise<void> => {
|
||||
);
|
||||
}
|
||||
} catch (error: any) {
|
||||
void tracker.trackEvent(TRACK_EVENTS.removeFailed, TRACK_PAGES.remove, {
|
||||
a: resolveTrackAction(TRACK_EVENTS.removeFailed),
|
||||
input_type: parsedInput.type,
|
||||
reason: getTrackErrorReason(error),
|
||||
});
|
||||
console.error(chalk.red(`Error: ${error.message}`));
|
||||
}
|
||||
return;
|
||||
@@ -172,6 +187,10 @@ export default async (options?: RemoveOptions): Promise<void> => {
|
||||
try {
|
||||
const success = await removeFromIpfs(parsedInput.value, parsedInput.type);
|
||||
if (success) {
|
||||
void tracker.trackEvent(TRACK_EVENTS.removeSuccess, TRACK_PAGES.remove, {
|
||||
a: resolveTrackAction(TRACK_EVENTS.removeSuccess),
|
||||
input_type: parsedInput.type,
|
||||
});
|
||||
console.log(
|
||||
chalk.cyan(
|
||||
figlet.textSync('Successful', { horizontalLayout: 'full' }),
|
||||
@@ -179,10 +198,19 @@ export default async (options?: RemoveOptions): Promise<void> => {
|
||||
);
|
||||
}
|
||||
} catch (error: any) {
|
||||
void tracker.trackEvent(TRACK_EVENTS.removeFailed, TRACK_PAGES.remove, {
|
||||
a: resolveTrackAction(TRACK_EVENTS.removeFailed),
|
||||
input_type: parsedInput.type,
|
||||
reason: getTrackErrorReason(error),
|
||||
});
|
||||
console.error(chalk.red(`Error: ${error.message}`));
|
||||
}
|
||||
}
|
||||
} catch (error: any) {
|
||||
void tracker.trackEvent(TRACK_EVENTS.removeFailed, TRACK_PAGES.remove, {
|
||||
a: resolveTrackAction(TRACK_EVENTS.removeFailed),
|
||||
reason: getTrackErrorReason(error),
|
||||
});
|
||||
console.error(chalk.red(`Error executing remove command: ${error.message}`));
|
||||
console.error(error.stack);
|
||||
}
|
||||
|
||||
+20
@@ -24,6 +24,12 @@ import {
|
||||
import { APP_CONFIG, getPinmeApiUrl } from './utils/config';
|
||||
import { uploadPath } from './services/uploadService';
|
||||
import { printHighlightedUrl } from './utils/urlDisplay';
|
||||
import tracker, { getTrackErrorReason } from './utils/tracker';
|
||||
import {
|
||||
TRACK_EVENTS,
|
||||
TRACK_PAGES,
|
||||
resolveTrackAction,
|
||||
} from './utils/trackerEvents';
|
||||
|
||||
const PROJECT_DIR = process.cwd();
|
||||
interface SaveOptions {
|
||||
@@ -423,8 +429,22 @@ export default async function saveCmd(options: SaveOptions): Promise<void> {
|
||||
'management',
|
||||
);
|
||||
console.log(chalk.green('\nDeployment complete.'));
|
||||
void tracker.trackEvent(TRACK_EVENTS.projectSaveSuccess, TRACK_PAGES.deploy, {
|
||||
a: resolveTrackAction(TRACK_EVENTS.projectSaveSuccess),
|
||||
project_name: projectName,
|
||||
has_domain: Boolean(options.domain),
|
||||
domain_type: options.domain
|
||||
? (isDnsDomain(options.domain) ? 'dns' : 'pinme_subdomain')
|
||||
: undefined,
|
||||
});
|
||||
process.exit(0);
|
||||
} catch (error: any) {
|
||||
void tracker.trackEvent(TRACK_EVENTS.projectSaveFailed, TRACK_PAGES.deploy, {
|
||||
a: resolveTrackAction(TRACK_EVENTS.projectSaveFailed),
|
||||
project_name: options.projectName || options.name,
|
||||
has_domain: Boolean(options.domain),
|
||||
reason: getTrackErrorReason(error),
|
||||
});
|
||||
printCliError(error, 'Save failed.');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
+16
-1
@@ -3,6 +3,12 @@ import inquirer from 'inquirer';
|
||||
import { setAuthToken } from './utils/webLogin';
|
||||
import { getDeviceId } from './utils/getDeviceId';
|
||||
import { bindAnonymousDevice } from './utils/pinmeApi';
|
||||
import tracker, { getTrackErrorReason } from './utils/tracker';
|
||||
import {
|
||||
TRACK_EVENTS,
|
||||
TRACK_PAGES,
|
||||
resolveTrackAction,
|
||||
} from './utils/trackerEvents';
|
||||
|
||||
export default async function setAppKeyCmd(): Promise<void> {
|
||||
try {
|
||||
@@ -33,8 +39,17 @@ export default async function setAppKeyCmd(): Promise<void> {
|
||||
} else {
|
||||
console.log(chalk.yellow('Anonymous history merge not confirmed. You may retry later.'));
|
||||
}
|
||||
|
||||
void tracker.trackEvent(TRACK_EVENTS.appKeySetSuccess, TRACK_PAGES.auth, {
|
||||
a: resolveTrackAction(TRACK_EVENTS.appKeySetSuccess),
|
||||
merged_anonymous_history: ok,
|
||||
has_token_address: Boolean(saved.address),
|
||||
});
|
||||
} catch (e: any) {
|
||||
void tracker.trackEvent(TRACK_EVENTS.appKeySetFailed, TRACK_PAGES.auth, {
|
||||
a: resolveTrackAction(TRACK_EVENTS.appKeySetFailed),
|
||||
reason: getTrackErrorReason(e),
|
||||
});
|
||||
console.log(chalk.red(`Failed to set AppKey: ${e?.message || e}`));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+18
-1
@@ -1,10 +1,20 @@
|
||||
import chalk from 'chalk';
|
||||
import { getAuthConfig } from './utils/webLogin';
|
||||
import tracker, { getTrackErrorReason } from './utils/tracker';
|
||||
import {
|
||||
TRACK_EVENTS,
|
||||
TRACK_PAGES,
|
||||
resolveTrackAction,
|
||||
} from './utils/trackerEvents';
|
||||
|
||||
export default function showAppKeyCmd(): void {
|
||||
try {
|
||||
const auth = getAuthConfig();
|
||||
if (!auth) {
|
||||
void tracker.trackEvent(TRACK_EVENTS.appKeyShownFailed, TRACK_PAGES.auth, {
|
||||
a: resolveTrackAction(TRACK_EVENTS.appKeyShownFailed),
|
||||
reason: 'no_appkey_found',
|
||||
});
|
||||
console.log(chalk.yellow('No AppKey found. Please set your AppKey first.'));
|
||||
console.log(chalk.gray('Run: pinme set-appkey <AppKey>'));
|
||||
return;
|
||||
@@ -32,8 +42,15 @@ export default function showAppKeyCmd(): void {
|
||||
} else {
|
||||
console.log(chalk.cyan(` AppKey: ${'*'.repeat(combined.length)}`));
|
||||
}
|
||||
void tracker.trackEvent(TRACK_EVENTS.appKeyShownSuccess, TRACK_PAGES.auth, {
|
||||
a: resolveTrackAction(TRACK_EVENTS.appKeyShownSuccess),
|
||||
has_token_address: Boolean(auth.address),
|
||||
});
|
||||
} catch (e: any) {
|
||||
void tracker.trackEvent(TRACK_EVENTS.appKeyShownFailed, TRACK_PAGES.auth, {
|
||||
a: resolveTrackAction(TRACK_EVENTS.appKeyShownFailed),
|
||||
reason: getTrackErrorReason(e),
|
||||
});
|
||||
console.log(chalk.red(`Failed to show AppKey: ${e?.message || e}`));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -9,6 +9,12 @@ import {
|
||||
printCliError,
|
||||
} from './utils/cliError';
|
||||
import { getPinmeApiUrl } from './utils/config';
|
||||
import tracker, { getTrackErrorReason } from './utils/tracker';
|
||||
import {
|
||||
TRACK_EVENTS,
|
||||
TRACK_PAGES,
|
||||
resolveTrackAction,
|
||||
} from './utils/trackerEvents';
|
||||
|
||||
const PROJECT_DIR = process.cwd();
|
||||
interface UpdateDbOptions {
|
||||
@@ -178,8 +184,26 @@ export default async function updateDbCmd(options?: UpdateDbOptions): Promise<vo
|
||||
await updateDb(sqlFiles, projectName);
|
||||
|
||||
console.log(chalk.green('\nDatabase update complete.'));
|
||||
void tracker.trackEvent(
|
||||
TRACK_EVENTS.projectUpdateDbSuccess,
|
||||
TRACK_PAGES.deploy,
|
||||
{
|
||||
a: resolveTrackAction(TRACK_EVENTS.projectUpdateDbSuccess),
|
||||
project_name: projectName,
|
||||
sql_file_count: sqlFiles.length,
|
||||
},
|
||||
);
|
||||
process.exit(0);
|
||||
} catch (error: any) {
|
||||
void tracker.trackEvent(
|
||||
TRACK_EVENTS.projectUpdateDbFailed,
|
||||
TRACK_PAGES.deploy,
|
||||
{
|
||||
a: resolveTrackAction(TRACK_EVENTS.projectUpdateDbFailed),
|
||||
project_name: options?.projectName || options?.name,
|
||||
reason: getTrackErrorReason(error),
|
||||
},
|
||||
);
|
||||
printCliError(error, 'Database update failed.');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
@@ -11,6 +11,12 @@ import {
|
||||
import { APP_CONFIG } from './utils/config';
|
||||
import { uploadPath } from './services/uploadService';
|
||||
import { printHighlightedUrl } from './utils/urlDisplay';
|
||||
import tracker, { getTrackErrorReason } from './utils/tracker';
|
||||
import {
|
||||
TRACK_EVENTS,
|
||||
TRACK_PAGES,
|
||||
resolveTrackAction,
|
||||
} from './utils/trackerEvents';
|
||||
|
||||
const PROJECT_DIR = process.cwd();
|
||||
|
||||
@@ -123,8 +129,25 @@ export default async function updateWebCmd(options?: UpdateWebOptions): Promise<
|
||||
await deployFrontend(projectName);
|
||||
|
||||
console.log(chalk.green('\nWeb update complete.'));
|
||||
void tracker.trackEvent(
|
||||
TRACK_EVENTS.projectUpdateWebSuccess,
|
||||
TRACK_PAGES.deploy,
|
||||
{
|
||||
a: resolveTrackAction(TRACK_EVENTS.projectUpdateWebSuccess),
|
||||
project_name: projectName,
|
||||
},
|
||||
);
|
||||
process.exit(0);
|
||||
} catch (error: any) {
|
||||
void tracker.trackEvent(
|
||||
TRACK_EVENTS.projectUpdateWebFailed,
|
||||
TRACK_PAGES.deploy,
|
||||
{
|
||||
a: resolveTrackAction(TRACK_EVENTS.projectUpdateWebFailed),
|
||||
project_name: options?.projectName || options?.name,
|
||||
reason: getTrackErrorReason(error),
|
||||
},
|
||||
);
|
||||
printCliError(error, 'Web update failed.');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
@@ -11,6 +11,12 @@ import {
|
||||
printCliError,
|
||||
} from './utils/cliError';
|
||||
import { getPinmeApiUrl } from './utils/config';
|
||||
import tracker, { getTrackErrorReason } from './utils/tracker';
|
||||
import {
|
||||
TRACK_EVENTS,
|
||||
TRACK_PAGES,
|
||||
resolveTrackAction,
|
||||
} from './utils/trackerEvents';
|
||||
|
||||
const PROJECT_DIR = process.cwd();
|
||||
interface UpdateWorkerOptions {
|
||||
@@ -235,8 +241,26 @@ export default async function updateWorkerCmd(options?: UpdateWorkerOptions): Pr
|
||||
await updateWorker(workerJsPath, modulePaths, metadata, projectName);
|
||||
|
||||
console.log(chalk.green('\nWorker update complete.'));
|
||||
void tracker.trackEvent(
|
||||
TRACK_EVENTS.projectUpdateWorkerSuccess,
|
||||
TRACK_PAGES.deploy,
|
||||
{
|
||||
a: resolveTrackAction(TRACK_EVENTS.projectUpdateWorkerSuccess),
|
||||
project_name: projectName,
|
||||
module_count: modulePaths.length,
|
||||
},
|
||||
);
|
||||
process.exit(0);
|
||||
} catch (error: any) {
|
||||
void tracker.trackEvent(
|
||||
TRACK_EVENTS.projectUpdateWorkerFailed,
|
||||
TRACK_PAGES.deploy,
|
||||
{
|
||||
a: resolveTrackAction(TRACK_EVENTS.projectUpdateWorkerFailed),
|
||||
project_name: options?.projectName || options?.name,
|
||||
reason: getTrackErrorReason(error),
|
||||
},
|
||||
);
|
||||
printCliError(error, 'Worker update failed.');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
+168
-11
@@ -19,6 +19,15 @@ import {
|
||||
import { printCliError, printRechargeUrl } from './utils/cliError';
|
||||
import { resolveUploadUrls, uploadPath } from './services/uploadService';
|
||||
import { printHighlightedUrl } from './utils/urlDisplay';
|
||||
import tracker, {
|
||||
getPathKind,
|
||||
getTrackErrorReason,
|
||||
} from './utils/tracker';
|
||||
import {
|
||||
TRACK_EVENTS,
|
||||
TRACK_PAGES,
|
||||
resolveTrackAction,
|
||||
} from './utils/trackerEvents';
|
||||
|
||||
import { checkNodeVersion } from './utils/checkNodeVersion';
|
||||
checkNodeVersion();
|
||||
@@ -44,13 +53,15 @@ interface UploadOptions {
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
async function printUploadUrls(result: {
|
||||
contentHash: string;
|
||||
shortUrl?: string;
|
||||
pinmeUrl?: string;
|
||||
dnsUrl?: string;
|
||||
}): Promise<void> {
|
||||
const projectName = APP_CONFIG.pinmeProjectName;
|
||||
async function printUploadUrls(
|
||||
result: {
|
||||
contentHash: string;
|
||||
shortUrl?: string;
|
||||
pinmeUrl?: string;
|
||||
dnsUrl?: string;
|
||||
},
|
||||
projectName?: string,
|
||||
): Promise<void> {
|
||||
const { publicUrl, managementUrl } = await resolveUploadUrls(
|
||||
result.contentHash,
|
||||
{
|
||||
@@ -65,6 +76,77 @@ async function printUploadUrls(result: {
|
||||
printHighlightedUrl('Management URL', managementUrl, 'management');
|
||||
}
|
||||
|
||||
function readProjectNameFromConfig(configPath: string): string | undefined {
|
||||
try {
|
||||
const config = fs.readFileSync(configPath, 'utf-8');
|
||||
const match = config.match(/project_name\s*=\s*"([^"]+)"/);
|
||||
return match?.[1]?.trim() || undefined;
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
function findPinmeConfig(startPath: string): string | undefined {
|
||||
try {
|
||||
let current = fs.statSync(startPath).isDirectory()
|
||||
? startPath
|
||||
: path.dirname(startPath);
|
||||
|
||||
while (true) {
|
||||
const configPath = path.join(current, 'pinme.toml');
|
||||
if (fs.existsSync(configPath)) {
|
||||
return configPath;
|
||||
}
|
||||
|
||||
const parent = path.dirname(current);
|
||||
if (parent === current) {
|
||||
return undefined;
|
||||
}
|
||||
current = parent;
|
||||
}
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
function shouldInferProjectNameFromUploadPath(targetPath: string): boolean {
|
||||
try {
|
||||
return (
|
||||
fs.statSync(targetPath).isDirectory() &&
|
||||
path.basename(targetPath) === 'dist'
|
||||
);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function resolveUploadProjectName(targetPath: string): string | undefined {
|
||||
if (APP_CONFIG.pinmeProjectName) {
|
||||
return APP_CONFIG.pinmeProjectName;
|
||||
}
|
||||
|
||||
if (!shouldInferProjectNameFromUploadPath(targetPath)) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const configPaths = [
|
||||
findPinmeConfig(targetPath),
|
||||
findPinmeConfig(process.cwd()),
|
||||
].filter(
|
||||
(value, index, values): value is string =>
|
||||
Boolean(value) && values.indexOf(value) === index,
|
||||
);
|
||||
|
||||
for (const configPath of configPaths) {
|
||||
const projectName = readProjectNameFromConfig(configPath);
|
||||
if (projectName) {
|
||||
return projectName;
|
||||
}
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function getDomainFromArgs(): string | null {
|
||||
const args = process.argv.slice(2);
|
||||
const dIdx = args.findIndex((a) => a === '--domain' || a === '-d');
|
||||
@@ -113,6 +195,7 @@ async function bindDomain(
|
||||
authConfig: { address: string; token: string },
|
||||
): Promise<boolean> {
|
||||
const displayDomain = normalizeDomain(domain);
|
||||
const domainType = isDns ? 'dns' : 'pinme_subdomain';
|
||||
|
||||
if (isDns) {
|
||||
// DNS domain binding
|
||||
@@ -124,9 +207,22 @@ async function bindDomain(
|
||||
authConfig.token,
|
||||
);
|
||||
if (dnsResult.code !== 200) {
|
||||
void tracker.trackEvent(TRACK_EVENTS.domainBindFailed, TRACK_PAGES.domain, {
|
||||
a: resolveTrackAction(TRACK_EVENTS.domainBindFailed),
|
||||
domain_type: domainType,
|
||||
domain_name: displayDomain,
|
||||
bind_source: 'upload',
|
||||
reason: dnsResult.msg || 'dns_bind_failed',
|
||||
});
|
||||
console.log(chalk.red(`DNS binding failed: ${dnsResult.msg}`));
|
||||
return false;
|
||||
}
|
||||
void tracker.trackEvent(TRACK_EVENTS.domainBindSuccess, TRACK_PAGES.domain, {
|
||||
a: resolveTrackAction(TRACK_EVENTS.domainBindSuccess),
|
||||
domain_type: domainType,
|
||||
domain_name: displayDomain,
|
||||
bind_source: 'upload',
|
||||
});
|
||||
console.log(chalk.green(`DNS bind success: ${displayDomain}`));
|
||||
console.log(chalk.white(`Visit: https://${displayDomain}`));
|
||||
console.log(
|
||||
@@ -139,9 +235,22 @@ async function bindDomain(
|
||||
console.log(chalk.blue('Binding Pinme subdomain...'));
|
||||
const ok = await bindPinmeDomain(displayDomain, contentHash);
|
||||
if (!ok) {
|
||||
void tracker.trackEvent(TRACK_EVENTS.domainBindFailed, TRACK_PAGES.domain, {
|
||||
a: resolveTrackAction(TRACK_EVENTS.domainBindFailed),
|
||||
domain_type: domainType,
|
||||
domain_name: displayDomain,
|
||||
bind_source: 'upload',
|
||||
reason: 'pinme_bind_failed',
|
||||
});
|
||||
console.log(chalk.red('Binding failed. Please try again later.'));
|
||||
return false;
|
||||
}
|
||||
void tracker.trackEvent(TRACK_EVENTS.domainBindSuccess, TRACK_PAGES.domain, {
|
||||
a: resolveTrackAction(TRACK_EVENTS.domainBindSuccess),
|
||||
domain_type: domainType,
|
||||
domain_name: displayDomain,
|
||||
bind_source: 'upload',
|
||||
});
|
||||
console.log(chalk.green(`Bind success: ${displayDomain}`));
|
||||
const rootDomain = await (await import('./utils/pinmeApi')).getRootDomain();
|
||||
console.log(chalk.white(`Visit: https://${displayDomain}.${rootDomain}`));
|
||||
@@ -181,6 +290,11 @@ export default async (options?: UploadOptions): Promise<void> => {
|
||||
console.log(chalk.red(`path ${argPath} does not exist`));
|
||||
return;
|
||||
}
|
||||
const pathKind = getPathKind(absolutePath);
|
||||
const projectName = resolveUploadProjectName(absolutePath);
|
||||
if (projectName) {
|
||||
console.log(chalk.gray(`Project: ${projectName}`));
|
||||
}
|
||||
|
||||
// Auto-detect domain type
|
||||
const isDns = dnsArg || (domainArg ? isDnsDomain(domainArg) : false);
|
||||
@@ -243,23 +357,42 @@ export default async (options?: UploadOptions): Promise<void> => {
|
||||
let result;
|
||||
try {
|
||||
result = await uploadPath(absolutePath, {
|
||||
projectName: APP_CONFIG.pinmeProjectName,
|
||||
projectName,
|
||||
uid: authConfig?.address,
|
||||
});
|
||||
} catch (error: any) {
|
||||
void tracker.trackEvent(TRACK_EVENTS.uploadFailed, TRACK_PAGES.upload, {
|
||||
a: resolveTrackAction(TRACK_EVENTS.uploadFailed),
|
||||
path_kind: pathKind,
|
||||
has_domain: Boolean(domainArg),
|
||||
reason: getTrackErrorReason(error),
|
||||
});
|
||||
printCliError(error, 'Upload failed.');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (!result) {
|
||||
void tracker.trackEvent(TRACK_EVENTS.uploadFailed, TRACK_PAGES.upload, {
|
||||
a: resolveTrackAction(TRACK_EVENTS.uploadFailed),
|
||||
path_kind: pathKind,
|
||||
has_domain: Boolean(domainArg),
|
||||
reason: 'no_result_returned',
|
||||
});
|
||||
console.error(chalk.red('Upload failed: no result returned'));
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
void tracker.trackEvent(TRACK_EVENTS.uploadSuccess, TRACK_PAGES.upload, {
|
||||
a: resolveTrackAction(TRACK_EVENTS.uploadSuccess),
|
||||
path_kind: pathKind,
|
||||
has_domain: Boolean(domainArg),
|
||||
project_name: projectName,
|
||||
});
|
||||
|
||||
console.log(
|
||||
chalk.cyan(figlet.textSync('Successful', { horizontalLayout: 'full' })),
|
||||
);
|
||||
await printUploadUrls(result);
|
||||
await printUploadUrls(result, projectName);
|
||||
|
||||
// optional: bind domain after upload
|
||||
if (domainArg) {
|
||||
@@ -297,6 +430,11 @@ export default async (options?: UploadOptions): Promise<void> => {
|
||||
console.log(chalk.red(`path ${answer.path} does not exist`));
|
||||
return;
|
||||
}
|
||||
const pathKind = getPathKind(absolutePath);
|
||||
const projectName = resolveUploadProjectName(absolutePath);
|
||||
if (projectName) {
|
||||
console.log(chalk.gray(`Project: ${projectName}`));
|
||||
}
|
||||
|
||||
// Auto-detect domain type
|
||||
const isDns = dnsArg || (domainArg ? isDnsDomain(domainArg) : false);
|
||||
@@ -359,23 +497,42 @@ export default async (options?: UploadOptions): Promise<void> => {
|
||||
let result;
|
||||
try {
|
||||
result = await uploadPath(absolutePath, {
|
||||
projectName: APP_CONFIG.pinmeProjectName,
|
||||
projectName,
|
||||
uid: authConfig?.address,
|
||||
});
|
||||
} catch (error: any) {
|
||||
void tracker.trackEvent(TRACK_EVENTS.uploadFailed, TRACK_PAGES.upload, {
|
||||
a: resolveTrackAction(TRACK_EVENTS.uploadFailed),
|
||||
path_kind: pathKind,
|
||||
has_domain: Boolean(domainArg),
|
||||
reason: getTrackErrorReason(error),
|
||||
});
|
||||
printCliError(error, 'Upload failed.');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (!result) {
|
||||
void tracker.trackEvent(TRACK_EVENTS.uploadFailed, TRACK_PAGES.upload, {
|
||||
a: resolveTrackAction(TRACK_EVENTS.uploadFailed),
|
||||
path_kind: pathKind,
|
||||
has_domain: Boolean(domainArg),
|
||||
reason: 'no_result_returned',
|
||||
});
|
||||
console.error(chalk.red('Upload failed: no result returned'));
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
void tracker.trackEvent(TRACK_EVENTS.uploadSuccess, TRACK_PAGES.upload, {
|
||||
a: resolveTrackAction(TRACK_EVENTS.uploadSuccess),
|
||||
path_kind: pathKind,
|
||||
has_domain: Boolean(domainArg),
|
||||
project_name: projectName,
|
||||
});
|
||||
|
||||
console.log(
|
||||
chalk.cyan(figlet.textSync('Successful', { horizontalLayout: 'full' })),
|
||||
);
|
||||
await printUploadUrls(result);
|
||||
await printUploadUrls(result, projectName);
|
||||
if (domainArg) {
|
||||
console.log(
|
||||
chalk.blue(
|
||||
|
||||
+77
-47
@@ -5,6 +5,12 @@ import dayjs from 'dayjs';
|
||||
import chalk from 'chalk';
|
||||
import { formatSize } from './uploadLimits';
|
||||
import { getRootDomain } from './pinmeApi';
|
||||
import tracker, { getTrackErrorReason } from './tracker';
|
||||
import {
|
||||
TRACK_EVENTS,
|
||||
TRACK_PAGES,
|
||||
resolveTrackAction,
|
||||
} from './trackerEvents';
|
||||
|
||||
// history file path
|
||||
const HISTORY_DIR = path.join(os.homedir(), '.pinme');
|
||||
@@ -144,57 +150,72 @@ async function formatHistoryUrl(
|
||||
|
||||
// display the upload history
|
||||
const displayUploadHistory = async (limit: number = 10): Promise<void> => {
|
||||
const history = getUploadHistory(limit);
|
||||
|
||||
if (history.length === 0) {
|
||||
console.log(chalk.yellow('No upload history found.'));
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(chalk.cyan('Upload History:'));
|
||||
console.log(chalk.cyan('-'.repeat(80)));
|
||||
let rootDomain: string | null = null;
|
||||
try {
|
||||
rootDomain = await getRootDomain();
|
||||
} catch {
|
||||
rootDomain = null;
|
||||
}
|
||||
|
||||
// Display recent records, up to limit records
|
||||
const recentHistory = history.slice(-limit);
|
||||
|
||||
for (const [index, item] of recentHistory.entries()) {
|
||||
console.log(chalk.green(`${index + 1}. ${item.filename}`));
|
||||
console.log(chalk.white(` Path: ${item.path}`));
|
||||
console.log(chalk.white(` IPFS CID: ${item.contentHash}`));
|
||||
const preferredUrl =
|
||||
(await formatHistoryUrl(item.dnsUrl)) ||
|
||||
(await formatHistoryUrl(item.pinmeUrl, {
|
||||
appendRootDomain: true,
|
||||
rootDomain,
|
||||
})) ||
|
||||
(await formatHistoryUrl(item.shortUrl, {
|
||||
appendRootDomain: true,
|
||||
rootDomain,
|
||||
}));
|
||||
if (preferredUrl) {
|
||||
console.log(chalk.white(` URL: ${preferredUrl}`));
|
||||
}
|
||||
console.log(chalk.white(` Size: ${formatSize(item.size)}`));
|
||||
console.log(chalk.white(` Files: ${item.fileCount}`));
|
||||
console.log(chalk.white(` Type: ${item.type === 'directory' ? 'Directory' : 'File'}`));
|
||||
if (item.timestamp) {
|
||||
console.log(chalk.white(` Date: ${new Date(item.timestamp).toLocaleString()}`));
|
||||
const history = getUploadHistory(limit);
|
||||
|
||||
void tracker.trackEvent(TRACK_EVENTS.uploadHistoryViewed, TRACK_PAGES.upload, {
|
||||
a: resolveTrackAction(TRACK_EVENTS.uploadHistoryViewed),
|
||||
record_count: history.length,
|
||||
limit,
|
||||
});
|
||||
|
||||
if (history.length === 0) {
|
||||
console.log(chalk.yellow('No upload history found.'));
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(chalk.cyan('Upload History:'));
|
||||
console.log(chalk.cyan('-'.repeat(80)));
|
||||
let rootDomain: string | null = null;
|
||||
try {
|
||||
rootDomain = await getRootDomain();
|
||||
} catch {
|
||||
rootDomain = null;
|
||||
}
|
||||
|
||||
// Display recent records, up to limit records
|
||||
const recentHistory = history.slice(-limit);
|
||||
|
||||
for (const [index, item] of recentHistory.entries()) {
|
||||
console.log(chalk.green(`${index + 1}. ${item.filename}`));
|
||||
console.log(chalk.white(` Path: ${item.path}`));
|
||||
console.log(chalk.white(` IPFS CID: ${item.contentHash}`));
|
||||
const preferredUrl =
|
||||
(await formatHistoryUrl(item.dnsUrl)) ||
|
||||
(await formatHistoryUrl(item.pinmeUrl, {
|
||||
appendRootDomain: true,
|
||||
rootDomain,
|
||||
})) ||
|
||||
(await formatHistoryUrl(item.shortUrl, {
|
||||
appendRootDomain: true,
|
||||
rootDomain,
|
||||
}));
|
||||
if (preferredUrl) {
|
||||
console.log(chalk.white(` URL: ${preferredUrl}`));
|
||||
}
|
||||
console.log(chalk.white(` Size: ${formatSize(item.size)}`));
|
||||
console.log(chalk.white(` Files: ${item.fileCount}`));
|
||||
console.log(chalk.white(` Type: ${item.type === 'directory' ? 'Directory' : 'File'}`));
|
||||
if (item.timestamp) {
|
||||
console.log(chalk.white(` Date: ${new Date(item.timestamp).toLocaleString()}`));
|
||||
}
|
||||
console.log(chalk.cyan('-'.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)}`));
|
||||
} catch (error: any) {
|
||||
void tracker.trackEvent(TRACK_EVENTS.uploadHistoryFailed, TRACK_PAGES.upload, {
|
||||
a: resolveTrackAction(TRACK_EVENTS.uploadHistoryFailed),
|
||||
action: 'view',
|
||||
reason: getTrackErrorReason(error),
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
|
||||
// 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
|
||||
@@ -202,9 +223,18 @@ const clearUploadHistory = (): boolean => {
|
||||
try {
|
||||
ensureHistoryDir();
|
||||
fs.writeJsonSync(HISTORY_FILE, { uploads: [] });
|
||||
void tracker.trackEvent(TRACK_EVENTS.uploadHistoryCleared, TRACK_PAGES.upload, {
|
||||
a: resolveTrackAction(TRACK_EVENTS.uploadHistoryCleared),
|
||||
cleared: true,
|
||||
});
|
||||
console.log(chalk.green('Upload history cleared successfully.'));
|
||||
return true;
|
||||
} catch (error: any) {
|
||||
void tracker.trackEvent(TRACK_EVENTS.uploadHistoryFailed, TRACK_PAGES.upload, {
|
||||
a: resolveTrackAction(TRACK_EVENTS.uploadHistoryFailed),
|
||||
action: 'clear',
|
||||
reason: getTrackErrorReason(error),
|
||||
});
|
||||
console.error(chalk.red(`Error clearing upload history: ${error.message}`));
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,267 @@
|
||||
import os from 'os';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import { spawn } from 'child_process';
|
||||
import { version } from '../../package.json';
|
||||
import { getUid } from './getDeviceId';
|
||||
|
||||
export interface TrackData {
|
||||
[key: string]: string | number | boolean | null | undefined;
|
||||
}
|
||||
|
||||
const REQUEST_TIMEOUT_MS = 1500;
|
||||
const DEFAULT_GATEWAY = 'https://pinme.dev';
|
||||
const DEFAULT_PRODUCT = 'pinme-cli';
|
||||
|
||||
const ACTION_OVERRIDES: Record<string, string> = {
|
||||
cli_login_success: 'success',
|
||||
cli_login_failed: 'fail',
|
||||
appkey_set_success: 'success',
|
||||
appkey_set_failed: 'fail',
|
||||
appkey_shown_success: 'view',
|
||||
appkey_shown_failed: 'fail',
|
||||
my_domains_success: 'view',
|
||||
my_domains_failed: 'fail',
|
||||
wallet_balance_success: 'view',
|
||||
wallet_balance_failed: 'fail',
|
||||
upload_history_viewed: 'view',
|
||||
upload_history_cleared: 'click',
|
||||
upload_history_failed: 'fail',
|
||||
};
|
||||
|
||||
const TRACK_CHILD_SCRIPT = `
|
||||
const rawUrl = process.argv[1];
|
||||
if (!rawUrl) process.exit(0);
|
||||
try {
|
||||
const transport = rawUrl.startsWith('https:') ? require('https') : require('http');
|
||||
const req = transport.get(rawUrl, {
|
||||
headers: {
|
||||
'User-Agent': 'Pinme-CLI-Tracker'
|
||||
}
|
||||
}, (res) => {
|
||||
res.resume();
|
||||
res.on('end', () => process.exit(0));
|
||||
});
|
||||
req.setTimeout(${REQUEST_TIMEOUT_MS}, () => req.destroy());
|
||||
req.on('error', () => process.exit(0));
|
||||
req.on('close', () => process.exit(0));
|
||||
} catch (_) {
|
||||
process.exit(0);
|
||||
}
|
||||
`;
|
||||
|
||||
function trimTrailingSlash(value: string): string {
|
||||
return value.replace(/\/+$/, '');
|
||||
}
|
||||
|
||||
function shouldDisableTracking(): boolean {
|
||||
return (
|
||||
process.env.PINME_TRACKING_DISABLED === '1' ||
|
||||
process.env.DO_NOT_TRACK === '1'
|
||||
);
|
||||
}
|
||||
|
||||
function sanitizeTrackValue(value: unknown): string | undefined {
|
||||
if (value === undefined || value === null) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return String(value).trim().slice(0, 200);
|
||||
}
|
||||
|
||||
export function getTrackErrorReason(error: unknown): string {
|
||||
const candidate =
|
||||
(error as any)?.response?.data?.msg ||
|
||||
(error as any)?.response?.data?.message ||
|
||||
(error as any)?.message ||
|
||||
(error as any)?.toString?.() ||
|
||||
'unknown_error';
|
||||
|
||||
return sanitizeTrackValue(candidate) || 'unknown_error';
|
||||
}
|
||||
|
||||
function resolveTrackAction(event: string, data: TrackData = {}): string {
|
||||
const explicitAction = data.a;
|
||||
if (typeof explicitAction === 'string' && explicitAction.trim()) {
|
||||
return explicitAction.trim();
|
||||
}
|
||||
|
||||
if (ACTION_OVERRIDES[event]) {
|
||||
return ACTION_OVERRIDES[event];
|
||||
}
|
||||
|
||||
if (event.endsWith('_success')) {
|
||||
return 'success';
|
||||
}
|
||||
|
||||
if (event.endsWith('_failed') || event.endsWith('_fail')) {
|
||||
return 'fail';
|
||||
}
|
||||
|
||||
if (event.includes('click') || event.includes('copied')) {
|
||||
return 'click';
|
||||
}
|
||||
|
||||
if (event.includes('exposure')) {
|
||||
return 'exposure';
|
||||
}
|
||||
|
||||
if (event.includes('view')) {
|
||||
return 'view';
|
||||
}
|
||||
|
||||
if (event.endsWith('_started') || event.endsWith('_submit')) {
|
||||
return 'submit';
|
||||
}
|
||||
|
||||
return 'view';
|
||||
}
|
||||
|
||||
interface ProjectContext {
|
||||
projectName?: string;
|
||||
projectDir?: string;
|
||||
}
|
||||
|
||||
let cachedProjectContext: ProjectContext | null = null;
|
||||
let cachedProjectContextCwd: string | null = null;
|
||||
|
||||
function resolveProjectContext(): ProjectContext {
|
||||
const cwd = process.cwd();
|
||||
if (cachedProjectContext && cachedProjectContextCwd === cwd) {
|
||||
return cachedProjectContext;
|
||||
}
|
||||
|
||||
const context: ProjectContext = {};
|
||||
const configPath = path.join(cwd, 'pinme.toml');
|
||||
|
||||
if (fs.existsSync(configPath)) {
|
||||
try {
|
||||
const configContent = fs.readFileSync(configPath, 'utf8');
|
||||
const projectNameMatch = configContent.match(
|
||||
/project_name\s*=\s*"([^"]+)"/,
|
||||
);
|
||||
|
||||
context.projectName =
|
||||
sanitizeTrackValue(projectNameMatch?.[1]) ||
|
||||
sanitizeTrackValue(process.env.PINME_PROJECT_NAME);
|
||||
context.projectDir = sanitizeTrackValue(path.basename(cwd));
|
||||
} catch (_) {
|
||||
context.projectName = sanitizeTrackValue(process.env.PINME_PROJECT_NAME);
|
||||
}
|
||||
} else {
|
||||
context.projectName = sanitizeTrackValue(process.env.PINME_PROJECT_NAME);
|
||||
}
|
||||
|
||||
cachedProjectContext = context;
|
||||
cachedProjectContextCwd = cwd;
|
||||
return context;
|
||||
}
|
||||
|
||||
export function getPathKind(pathValue: string): string {
|
||||
try {
|
||||
const stat = fs.statSync(pathValue);
|
||||
if (stat.isDirectory()) {
|
||||
return 'directory';
|
||||
}
|
||||
if (stat.isFile()) {
|
||||
return 'file';
|
||||
}
|
||||
} catch (_) {
|
||||
return 'unknown';
|
||||
}
|
||||
|
||||
return 'unknown';
|
||||
}
|
||||
|
||||
class Tracker {
|
||||
private static instance: Tracker;
|
||||
private readonly gateway: string;
|
||||
private readonly product: string;
|
||||
private readonly source: string | undefined;
|
||||
private readonly disabled: boolean;
|
||||
|
||||
private constructor(gateway?: string, product?: string) {
|
||||
this.gateway = trimTrailingSlash(
|
||||
gateway || process.env.PINME_TRACKER_GATEWAY || DEFAULT_GATEWAY,
|
||||
);
|
||||
this.product = product || DEFAULT_PRODUCT;
|
||||
this.source = sanitizeTrackValue(process.env.PINME_TRACK_SOURCE);
|
||||
this.disabled = shouldDisableTracking();
|
||||
}
|
||||
|
||||
public static getInstance(gateway?: string, product?: string): Tracker {
|
||||
if (!Tracker.instance) {
|
||||
Tracker.instance = new Tracker(gateway, product);
|
||||
}
|
||||
return Tracker.instance;
|
||||
}
|
||||
|
||||
public trackEvent(
|
||||
event: string,
|
||||
page: string,
|
||||
data: TrackData = {},
|
||||
): Promise<void> {
|
||||
if (this.disabled || !this.gateway) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
try {
|
||||
const payload = this.buildPayload(event, page, data);
|
||||
const params = new URLSearchParams(payload).toString();
|
||||
const url = `${this.gateway}/track.gif?${params}`;
|
||||
this.dispatch(url);
|
||||
} catch (_) {
|
||||
// Tracking is best-effort and must never interrupt CLI flows.
|
||||
}
|
||||
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
private buildPayload(
|
||||
event: string,
|
||||
page: string,
|
||||
data: TrackData,
|
||||
): Record<string, string> {
|
||||
const projectContext = resolveProjectContext();
|
||||
const action = resolveTrackAction(event, data);
|
||||
const payload: TrackData = {
|
||||
...data,
|
||||
u: getUid(),
|
||||
s: this.source,
|
||||
pd: this.product,
|
||||
p: page,
|
||||
a: action,
|
||||
ev: event,
|
||||
event,
|
||||
project_name: projectContext.projectName,
|
||||
project_dir: projectContext.projectDir,
|
||||
cli_version: version,
|
||||
node_version: process.version,
|
||||
os: os.platform(),
|
||||
arch: os.arch(),
|
||||
};
|
||||
|
||||
const filtered: Record<string, string> = {};
|
||||
for (const [key, value] of Object.entries(payload)) {
|
||||
const normalized = sanitizeTrackValue(value);
|
||||
if (normalized) {
|
||||
filtered[key] = normalized;
|
||||
}
|
||||
}
|
||||
|
||||
return filtered;
|
||||
}
|
||||
|
||||
private dispatch(url: string): void {
|
||||
const child = spawn(process.execPath, ['-e', TRACK_CHILD_SCRIPT, url], {
|
||||
detached: true,
|
||||
stdio: 'ignore',
|
||||
windowsHide: true,
|
||||
});
|
||||
child.unref();
|
||||
}
|
||||
}
|
||||
|
||||
const tracker = Tracker.getInstance();
|
||||
|
||||
export default tracker;
|
||||
@@ -0,0 +1,114 @@
|
||||
export const TRACK_PAGES = {
|
||||
auth: 'cli_auth',
|
||||
login: 'cli_login',
|
||||
upload: 'cli_upload',
|
||||
import: 'cli_import',
|
||||
export: 'cli_export',
|
||||
remove: 'cli_remove',
|
||||
domain: 'cli_domain',
|
||||
wallet: 'cli_wallet',
|
||||
project: 'cli_project',
|
||||
deploy: 'cli_deploy',
|
||||
} as const;
|
||||
|
||||
export const TRACK_EVENTS = {
|
||||
cliLoginSuccess: 'cli_login_success',
|
||||
cliLoginFailed: 'cli_login_failed',
|
||||
appKeySetSuccess: 'appkey_set_success',
|
||||
appKeySetFailed: 'appkey_set_failed',
|
||||
logoutSuccess: 'logout_success',
|
||||
logoutFailed: 'logout_failed',
|
||||
|
||||
uploadSuccess: 'upload_success',
|
||||
uploadFailed: 'upload_failed',
|
||||
importSuccess: 'import_success',
|
||||
importFailed: 'import_failed',
|
||||
exportSuccess: 'export_success',
|
||||
exportFailed: 'export_failed',
|
||||
removeSuccess: 'remove_success',
|
||||
removeFailed: 'remove_failed',
|
||||
|
||||
domainBindSuccess: 'domain_bind_success',
|
||||
domainBindFailed: 'domain_bind_failed',
|
||||
myDomainsSuccess: 'my_domains_success',
|
||||
myDomainsFailed: 'my_domains_failed',
|
||||
walletBalanceSuccess: 'wallet_balance_success',
|
||||
walletBalanceFailed: 'wallet_balance_failed',
|
||||
|
||||
projectCreateSuccess: 'project_create_success',
|
||||
projectCreateFailed: 'project_create_failed',
|
||||
projectSaveSuccess: 'project_save_success',
|
||||
projectSaveFailed: 'project_save_failed',
|
||||
projectUpdateDbSuccess: 'project_update_db_success',
|
||||
projectUpdateDbFailed: 'project_update_db_failed',
|
||||
projectUpdateWorkerSuccess: 'project_update_worker_success',
|
||||
projectUpdateWorkerFailed: 'project_update_worker_failed',
|
||||
projectDeleteSuccess: 'project_delete_success',
|
||||
projectDeleteFailed: 'project_delete_failed',
|
||||
projectUpdateWebSuccess: 'project_update_web_success',
|
||||
projectUpdateWebFailed: 'project_update_web_failed',
|
||||
|
||||
appKeyShownSuccess: 'appkey_shown_success',
|
||||
appKeyShownFailed: 'appkey_shown_failed',
|
||||
uploadHistoryViewed: 'upload_history_viewed',
|
||||
uploadHistoryCleared: 'upload_history_cleared',
|
||||
uploadHistoryFailed: 'upload_history_failed',
|
||||
} as const;
|
||||
|
||||
export const TRACK_ACTIONS = {
|
||||
init: 'init',
|
||||
click: 'click',
|
||||
view: 'view',
|
||||
exposure: 'exposure',
|
||||
submit: 'submit',
|
||||
success: 'success',
|
||||
fail: 'fail',
|
||||
} as const;
|
||||
|
||||
export function resolveTrackAction(event: string): string {
|
||||
switch (event) {
|
||||
case TRACK_EVENTS.uploadHistoryViewed:
|
||||
case TRACK_EVENTS.myDomainsSuccess:
|
||||
case TRACK_EVENTS.walletBalanceSuccess:
|
||||
case TRACK_EVENTS.appKeyShownSuccess:
|
||||
return TRACK_ACTIONS.view;
|
||||
case TRACK_EVENTS.uploadHistoryCleared:
|
||||
return TRACK_ACTIONS.click;
|
||||
case TRACK_EVENTS.uploadSuccess:
|
||||
case TRACK_EVENTS.importSuccess:
|
||||
case TRACK_EVENTS.exportSuccess:
|
||||
case TRACK_EVENTS.removeSuccess:
|
||||
case TRACK_EVENTS.domainBindSuccess:
|
||||
case TRACK_EVENTS.cliLoginSuccess:
|
||||
case TRACK_EVENTS.appKeySetSuccess:
|
||||
case TRACK_EVENTS.logoutSuccess:
|
||||
case TRACK_EVENTS.projectCreateSuccess:
|
||||
case TRACK_EVENTS.projectSaveSuccess:
|
||||
case TRACK_EVENTS.projectUpdateDbSuccess:
|
||||
case TRACK_EVENTS.projectUpdateWorkerSuccess:
|
||||
case TRACK_EVENTS.projectUpdateWebSuccess:
|
||||
case TRACK_EVENTS.projectDeleteSuccess:
|
||||
return TRACK_ACTIONS.success;
|
||||
case TRACK_EVENTS.uploadFailed:
|
||||
case TRACK_EVENTS.importFailed:
|
||||
case TRACK_EVENTS.exportFailed:
|
||||
case TRACK_EVENTS.removeFailed:
|
||||
case TRACK_EVENTS.domainBindFailed:
|
||||
case TRACK_EVENTS.cliLoginFailed:
|
||||
case TRACK_EVENTS.appKeySetFailed:
|
||||
case TRACK_EVENTS.logoutFailed:
|
||||
case TRACK_EVENTS.myDomainsFailed:
|
||||
case TRACK_EVENTS.walletBalanceFailed:
|
||||
case TRACK_EVENTS.projectCreateFailed:
|
||||
case TRACK_EVENTS.projectSaveFailed:
|
||||
case TRACK_EVENTS.projectUpdateDbFailed:
|
||||
case TRACK_EVENTS.projectUpdateWorkerFailed:
|
||||
case TRACK_EVENTS.projectUpdateWebFailed:
|
||||
case TRACK_EVENTS.projectDeleteFailed:
|
||||
case TRACK_EVENTS.appKeyShownFailed:
|
||||
case TRACK_EVENTS.uploadHistoryFailed:
|
||||
return TRACK_ACTIONS.fail;
|
||||
default:
|
||||
return TRACK_ACTIONS.view;
|
||||
}
|
||||
}
|
||||
@@ -22,6 +22,7 @@ const RETRY_DELAY = APP_CONFIG.upload.retryDelayMs;
|
||||
const TIMEOUT = APP_CONFIG.upload.timeoutMs;
|
||||
const MAX_POLL_TIME = APP_CONFIG.upload.maxPollTimeMs;
|
||||
const POLL_INTERVAL = APP_CONFIG.upload.pollIntervalMs;
|
||||
const COMPLETE_TO_STATUS_DELAY = 5000;
|
||||
const PROGRESS_UPDATE_INTERVAL = 200; // ms
|
||||
const EXPECTED_UPLOAD_TIME = 60000; // 60 seconds
|
||||
const MAX_PROGRESS = 0.9; // 90%
|
||||
@@ -142,11 +143,18 @@ function createUploadBusinessError(
|
||||
);
|
||||
}
|
||||
|
||||
function logAxiosErrorDetails(prefix: string, error: any): void {
|
||||
function logAxiosErrorDetails(
|
||||
prefix: string,
|
||||
error: any,
|
||||
traceId?: string,
|
||||
): void {
|
||||
const status = error?.response?.status;
|
||||
const responseData = error?.response?.data;
|
||||
|
||||
console.error(`[pinme upload] ${prefix}`);
|
||||
if (traceId) {
|
||||
console.error(`[pinme upload] trace_id: ${traceId}`);
|
||||
}
|
||||
console.error(`[pinme upload] status: ${status ?? 'unknown'}`);
|
||||
|
||||
if (responseData === undefined) {
|
||||
@@ -161,6 +169,20 @@ function logAxiosErrorDetails(prefix: string, error: any): void {
|
||||
}
|
||||
}
|
||||
|
||||
function delay(ms: number): Promise<void> {
|
||||
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
function appendTraceIdToError(error: unknown, traceId: string): Error {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
|
||||
if (message.includes(`trace_id: ${traceId}`)) {
|
||||
return error instanceof Error ? error : new Error(message);
|
||||
}
|
||||
|
||||
return new Error(`${message} (trace_id: ${traceId})`);
|
||||
}
|
||||
|
||||
function isStorageLimitError(error: any): boolean {
|
||||
const message = extractAxiosErrorMessage(error).toLowerCase();
|
||||
return (
|
||||
@@ -672,10 +694,20 @@ async function getChunkStatus(
|
||||
);
|
||||
} catch (error) {
|
||||
if (axios.isAxiosError(error)) {
|
||||
logAxiosErrorDetails('up_status failed', error);
|
||||
throw formatAxiosError('Upload status check failed', error);
|
||||
logAxiosErrorDetails('up_status failed', error, sessionId);
|
||||
throw formatAxiosError(
|
||||
`Upload status check failed (trace_id: ${sessionId})`,
|
||||
error,
|
||||
);
|
||||
}
|
||||
throw error;
|
||||
console.error('[pinme upload] up_status failed');
|
||||
console.error(`[pinme upload] trace_id: ${sessionId}`);
|
||||
console.error(
|
||||
`[pinme upload] reason: ${
|
||||
error instanceof Error ? error.message : String(error)
|
||||
}`,
|
||||
);
|
||||
throw appendTraceIdToError(error, sessionId);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -686,7 +718,7 @@ async function monitorChunkProgress(
|
||||
progressBar?: StepProgressBar,
|
||||
): Promise<UploadResult | null> {
|
||||
let consecutiveErrors = 0;
|
||||
const startTime = Date.now();
|
||||
let startTime = Date.now();
|
||||
|
||||
// Start simulating progress
|
||||
if (progressBar) {
|
||||
@@ -694,6 +726,9 @@ async function monitorChunkProgress(
|
||||
}
|
||||
|
||||
try {
|
||||
await delay(COMPLETE_TO_STATUS_DELAY);
|
||||
startTime = Date.now();
|
||||
|
||||
while (Date.now() - startTime < MAX_POLL_TIME) {
|
||||
try {
|
||||
const status = await getChunkStatus(traceId, deviceId, options);
|
||||
@@ -725,7 +760,9 @@ async function monitorChunkProgress(
|
||||
}
|
||||
|
||||
const maxPollTimeMinutes = Math.floor(MAX_POLL_TIME / (60 * 1000));
|
||||
throw new Error(`Polling timeout after ${maxPollTimeMinutes} minutes`);
|
||||
throw new Error(
|
||||
`Polling timeout after ${maxPollTimeMinutes} minutes (trace_id: ${traceId})`,
|
||||
);
|
||||
} finally {
|
||||
// Ensure simulating progress is stopped
|
||||
if (progressBar) {
|
||||
|
||||
@@ -3,6 +3,12 @@ import { printCliError, printRechargeUrl } from './utils/cliError';
|
||||
import { getWalletRechargeUrl } from './utils/config';
|
||||
import { getWalletBalance } from './utils/pinmeApi';
|
||||
import { getAuthConfig } from './utils/webLogin';
|
||||
import tracker, { getTrackErrorReason } from './utils/tracker';
|
||||
import {
|
||||
TRACK_EVENTS,
|
||||
TRACK_PAGES,
|
||||
resolveTrackAction,
|
||||
} from './utils/trackerEvents';
|
||||
|
||||
export default async function walletBalanceCmd(): Promise<void> {
|
||||
try {
|
||||
@@ -16,10 +22,19 @@ export default async function walletBalanceCmd(): Promise<void> {
|
||||
const balance = Number(result.data?.wallet_balance_usd ?? 0);
|
||||
|
||||
if (!Number.isFinite(balance)) {
|
||||
void tracker.trackEvent(TRACK_EVENTS.walletBalanceFailed, TRACK_PAGES.wallet, {
|
||||
a: resolveTrackAction(TRACK_EVENTS.walletBalanceFailed),
|
||||
reason: 'invalid_balance_value',
|
||||
});
|
||||
console.log(chalk.red('Failed to parse wallet balance.'));
|
||||
return;
|
||||
}
|
||||
|
||||
void tracker.trackEvent(TRACK_EVENTS.walletBalanceSuccess, TRACK_PAGES.wallet, {
|
||||
a: resolveTrackAction(TRACK_EVENTS.walletBalanceSuccess),
|
||||
has_balance: balance > 0,
|
||||
balance_usd: balance.toFixed(2),
|
||||
});
|
||||
console.log(chalk.cyan('Wallet balance:'));
|
||||
console.log(chalk.green(` USD: $${balance.toFixed(2)}`));
|
||||
|
||||
@@ -28,6 +43,10 @@ export default async function walletBalanceCmd(): Promise<void> {
|
||||
printRechargeUrl(getWalletRechargeUrl());
|
||||
}
|
||||
} catch (e: any) {
|
||||
void tracker.trackEvent(TRACK_EVENTS.walletBalanceFailed, TRACK_PAGES.wallet, {
|
||||
a: resolveTrackAction(TRACK_EVENTS.walletBalanceFailed),
|
||||
reason: getTrackErrorReason(e),
|
||||
});
|
||||
printCliError(e, 'Failed to fetch wallet balance.');
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "pinme",
|
||||
"version": "2.0.4",
|
||||
"version": "2.0.6",
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user