@whtg/harvest-auto
Version:
一键导入日报到Harvest的CLI工具,One-click import daily reports to Harvest CLI tool
476 lines (456 loc) • 15.4 kB
JavaScript
const fs = require('fs');
const os = require('os');
const path = require('path');
const inquirer = require('inquirer');
const prompt = inquirer.createPromptModule();
const { MSG } = require('../i18n/messages');
/**
* 字段定义与说明:导出函数确保多处引用时字段/校验/i18n皆一致
*/
function getEnvFields(currentLang, MSG) {
return [
{
key: 'HARVEST_AUTO_LANG',
message:
MSG[currentLang].HARVEST_AUTO_LANG_LABEL || '界面/提示语言(CN/EN)',
validate: (v) =>
['CN', 'EN'].includes(v.trim().toUpperCase())
? true
: MSG[currentLang].HARVEST_AUTO_LANG_INVALID || '请输入 CN 或 EN',
filter: (v) => v.trim().toUpperCase(),
},
{
key: 'HARVEST_ACCOUNT_ID',
message: MSG[currentLang].HARVEST_ACCOUNT_ID,
validate: (v) => !!v || MSG[currentLang].NOT_EMPTY_ERROR,
},
{
key: 'HARVEST_TOKEN',
message: MSG[currentLang].HARVEST_TOKEN,
validate: (v) => !!v || MSG[currentLang].NOT_EMPTY_ERROR,
},
{
key: 'USER_AGENT',
message: MSG[currentLang].USER_AGENT,
validate: (v) => !!v || MSG[currentLang].NOT_EMPTY_ERROR,
},
{
key: 'PROJECT_MAP',
message: MSG[currentLang].PROJECT_MAP,
validate: (v) => {
if (!v) return MSG[currentLang].NOT_EMPTY_ERROR;
try {
const r = JSON.parse(v);
return typeof r === 'object' && !Array.isArray(r)
? true
: MSG[currentLang].PROJECT_MAP_ERR;
} catch {
return MSG[currentLang].PROJECT_MAP_ERR;
}
},
filter: (v) => JSON.stringify(JSON.parse(v)),
},
{
key: 'TASK_MAP',
message: MSG[currentLang].TASK_MAP,
validate: (v) => {
if (!v) return MSG[currentLang].NOT_EMPTY_ERROR;
try {
const r = JSON.parse(v);
return typeof r === 'object' && !Array.isArray(r)
? true
: MSG[currentLang].TASK_MAP_ERR;
} catch {
return MSG[currentLang].TASK_MAP_ERR;
}
},
filter: (v) => JSON.stringify(JSON.parse(v)),
},
];
}
/**
* 交互式创建~/.harvest-auto.env文件(首次)
*/
async function ensureUserEnvFileInteractive() {
try {
const homedir = os.homedir();
const userEnvPath = path.join(homedir, '.harvest-auto.env');
// 检查当前用户目录下文件是否存在
let isFirstRun = !fs.existsSync(userEnvPath);
let envContent = isFirstRun
? ''
: fs.readFileSync(userEnvPath, { encoding: 'utf-8' });
let matchedLang = envContent.match(/^\s*HARVEST_AUTO_LANG\s*=\s*(\w+)/m);
let currentLang = matchedLang?.[1] || null;
let osLocale = Intl.DateTimeFormat().resolvedOptions().locale;
// 检测系统语言
let guessedLang = osLocale.startsWith('zh') ? 'CN' : 'EN';
// 要求选择语言(首次,或之前没 LANG 字段的情况)
if (isFirstRun || !currentLang) {
console.log('\n');
// 系统语言提示,双语合成
console.log(
'\x1b[36m%s\x1b[0m',
MSG[guessedLang].SYSTEM_LANG_DETECTED(osLocale)
);
const { selectedLang } = await prompt([
{
type: 'list',
name: 'selectedLang',
message: MSG[guessedLang].LANGUAGE_CHOICE,
choices: [
{ name: MSG.CN.LANG_ZH, value: 'CN' },
{ name: MSG.CN.LANG_EN, value: 'EN' },
],
default: guessedLang,
},
]);
currentLang = selectedLang;
// 如果不是首次(已有env文件但无LANG),补写一行LANG
if (!isFirstRun) {
let newContent =
envContent.trim() + `\nHARVEST_AUTO_LANG=${currentLang}\n`;
fs.writeFileSync(userEnvPath, newContent, { encoding: 'utf-8' });
console.log('\x1b[32m%s\x1b[0m', MSG[currentLang].ENV_PATCHED);
}
// 如果是首次初始化并选择了推荐语言,不再重复输入该字段
// (在下方首次填写流程中直接“跳过”该字段)
}
if (isFirstRun) {
const FIELDS = getEnvFields(currentLang, MSG);
let envData = { LANG: currentLang };
for (const f of FIELDS) {
// 跳过 HARVEST_AUTO_LANG 字段的重复填写(已由上面语言选择赋值)
if (f.key === 'HARVEST_AUTO_LANG') {
envData[f.key] = currentLang;
continue;
}
let confirmed = false;
let val;
while (!confirmed) {
const { inputVal } = await prompt([
{
type: 'input',
name: 'inputVal',
message: f.message,
validate: f.validate,
},
]);
val = f.filter ? f.filter(inputVal) : inputVal;
const { isOk } = await prompt([
{
type: 'confirm',
name: 'isOk',
message: MSG[currentLang].CONFIRM(f.key, val),
default: true,
},
]);
confirmed = isOk;
}
envData[f.key] = val;
}
// 拼装env内容,LANG在最前
const envTemplate = [
`HARVEST_AUTO_LANG=${envData.LANG}`,
`HARVEST_ACCOUNT_ID=${envData.HARVEST_ACCOUNT_ID}`,
`HARVEST_TOKEN=${envData.HARVEST_TOKEN}`,
`USER_AGENT=${envData.USER_AGENT}`,
`PROJECT_MAP=${envData.PROJECT_MAP}`,
`TASK_MAP=${envData.TASK_MAP}`,
'',
].join('\n');
fs.writeFileSync(userEnvPath, envTemplate, { encoding: 'utf-8' });
console.log(
'\x1b[32m%s\x1b[0m',
MSG[currentLang].ENV_SUCCESS(userEnvPath)
);
return;
}
} catch (err) {
if (
err &&
(err.name === 'ExitPromptError' || err.message?.includes('SIGINT'))
) {
// 默认为中文友好提示,如需多语言可加参数
console.log('\x1b[33m操作已取消,初始化已终止。\x1b[0m');
process.exit(0);
}
throw err;
}
}
/**
* 交互式更改~/.harvest-auto.env字段,只修改选中的字段,支持多语言。
*/
async function changeUserEnvFileInteractive() {
try {
const homedir = os.homedir();
const userEnvPath = path.join(homedir, '.harvest-auto.env');
let envContent, matchedLang, currentLang;
if (!fs.existsSync(userEnvPath)) {
console.log(
'\x1b[31m%s\x1b[0m',
'配置文件不存在,请先执行任意填报或初始化.'
);
return;
}
envContent = fs.readFileSync(userEnvPath, { encoding: 'utf-8' });
matchedLang = envContent.match(/^\s*HARVEST_AUTO_LANG\s*=\s*(\w+)/m);
currentLang = matchedLang?.[1] || 'CN';
if (!MSG[currentLang]) currentLang = 'CN';
// 解析当前env内容
let currentEnv = {};
envContent.split(/\r?\n/).forEach((line) => {
let m = line.match(/^\s*([\w_]+)\s*=\s*(.+)$/);
if (m) {
currentEnv[m[1]] = m[2];
}
});
// 字段定义
let FIELDS = getEnvFields(currentLang, MSG);
// ==== 新交互:先问“全部还是部分字段” ====
const { doType } = await prompt([
{
type: 'list',
name: 'doType',
message: MSG[currentLang].CHANGE_ENV_CHOOSE_MODE,
choices: [
{ name: MSG[currentLang].CHANGE_ENV_CHOOSE_ALL, value: 'ALL' },
{ name: MSG[currentLang].CHANGE_ENV_CHOOSE_PART, value: 'PART' },
],
default: 'ALL',
},
]);
let keys = [];
if (doType === 'ALL') {
// 优先处理 lang 字段
let envData = { ...currentEnv };
const langField = 'HARVEST_AUTO_LANG';
const langFieldDesc = FIELDS.find((f) => f.key === langField);
let confirmedLang = false;
let langVal = currentEnv[langField] || currentLang;
while (!confirmedLang) {
const { inputLang } = await prompt([
{
type: 'list',
name: 'inputLang',
message:
langFieldDesc.message +
(langVal
? currentLang === 'CN'
? `(当前:${langVal})`
: ` (current: ${langVal})`
: ''),
default: langVal,
choices: [
{ name: MSG[currentLang].LANG_ZH || '简体中文(CN)', value: 'CN' },
{ name: MSG[currentLang].LANG_EN || 'English(EN)', value: 'EN' },
],
},
]);
let realLang = inputLang;
const { isOk } = await prompt([
{
type: 'confirm',
name: 'isOk',
message: MSG[currentLang].CONFIRM(langField, realLang),
default: true,
},
]);
confirmedLang = isOk;
if (confirmedLang) envData[langField] = realLang;
}
// 更新 lang 相关配置和字段定义
currentLang = envData[langField];
FIELDS = getEnvFields(currentLang, MSG);
// 其它字段顺序(除 lang 外的所有字段)
const keysNoLang = FIELDS.filter(
(f) => f.key !== 'HARVEST_AUTO_LANG'
).map((f) => f.key);
// 开始逐项变更其它字段
for (const key of keysNoLang) {
const desc = FIELDS.find((f) => f.key === key);
let confirmed = false;
let val = currentEnv[key] || '';
while (!confirmed) {
const { inputVal } = await prompt([
{
type: 'input',
name: 'inputVal',
message:
desc.message +
(val
? currentLang === 'CN'
? ` [当前值: ${val}]`
: ` [Current: ${val}]`
: ''),
default: val,
validate: desc.validate,
},
]);
let realInput = desc.filter ? desc.filter(inputVal) : inputVal;
const { isOk } = await prompt([
{
type: 'confirm',
name: 'isOk',
message: MSG[currentLang].CONFIRM(key, realInput),
default: true,
},
]);
confirmed = isOk;
if (confirmed) envData[key] = realInput;
}
}
// 重新组装env文件内容,LANG最前
const envLines = [
`HARVEST_AUTO_LANG=${envData['HARVEST_AUTO_LANG']}`,
...FIELDS.filter((f) => f.key !== 'HARVEST_AUTO_LANG').map(
(f) => `${f.key}=${envData[f.key] || ''}`
),
'',
];
fs.writeFileSync(userEnvPath, envLines.join('\n'), { encoding: 'utf-8' });
console.log(
'\x1b[32m%s\x1b[0m',
MSG[currentLang].CHANGE_ENV_DONE ||
MSG.CN.CHANGE_ENV_DONE ||
(currentLang === 'CN'
? '设置/修改已完成。'
: 'Configuration updated.')
);
} else {
// 部分字段模式
// 只显示可选字段
const { fieldsToChange } = await prompt([
{
type: 'checkbox',
name: 'fieldsToChange',
message: MSG[currentLang].CHANGE_ENV_SELECT_FIELDS,
choices: FIELDS.map((f) => ({
name: currentEnv[f.key]
? `${f.key}${
currentLang === 'CN'
? `(当前:${currentEnv[f.key]})`
: ` (current: ${currentEnv[f.key]})`
}`
: f.key,
value: f.key,
})),
pageSize: 10,
validate: (v) =>
v.length > 0 ? true : MSG[currentLang].CHANGE_ENV_MUST_SELECT,
},
]);
keys = fieldsToChange;
let envData = { ...currentEnv };
// 在部分字段修改时,交互过程中允许切换语言
let actualCurrentLang = currentLang;
for (const key of keys) {
// 每次都用最新的 actualCurrentLang 展示字段描述和消息
const curFields = getEnvFields(actualCurrentLang, MSG);
const desc = curFields.find((f) => f.key === key);
let confirmed = false;
let val =
envData[key] !== undefined ? envData[key] : currentEnv[key] || '';
while (!confirmed) {
let promptConfig;
if (key === 'HARVEST_AUTO_LANG') {
promptConfig = {
type: 'list',
name: 'inputVal',
message:
desc.message +
(val
? actualCurrentLang === 'CN'
? `(当前:${val})`
: ` (current: ${val})`
: ''),
default: val,
choices: [
{
name: MSG[actualCurrentLang].LANG_ZH || '简体中文(CN)',
value: 'CN',
},
{
name: MSG[actualCurrentLang].LANG_EN || 'English(EN)',
value: 'EN',
},
],
};
} else {
promptConfig = {
type: 'input',
name: 'inputVal',
message:
desc.message +
(val
? actualCurrentLang === 'CN'
? `(当前:${val})`
: ` (current: ${val})`
: ''),
default: val,
validate: desc.validate,
};
}
const { inputVal } = await prompt([promptConfig]);
let realInput = desc.filter ? desc.filter(inputVal) : inputVal;
// 用目标语言进行 HARVEST_AUTO_LANG 字段确认,其它字段用当前语种
let confirmLang = actualCurrentLang;
if (key === 'HARVEST_AUTO_LANG' && realInput) {
confirmLang = realInput;
}
const { isOk } = await prompt([
{
type: 'confirm',
name: 'isOk',
message: MSG[confirmLang].CONFIRM(key, realInput),
default: true,
},
]);
confirmed = isOk;
if (confirmed) {
envData[key] = realInput;
// 实时切换语言
if (
key === 'HARVEST_AUTO_LANG' &&
realInput &&
realInput !== actualCurrentLang
) {
actualCurrentLang = realInput;
}
}
}
}
// 重新组装env文件内容,LANG最前
const finishLang = envData['HARVEST_AUTO_LANG'] || actualCurrentLang;
const envLines = [
`HARVEST_AUTO_LANG=${finishLang}`,
...FIELDS.filter((f) => f.key !== 'HARVEST_AUTO_LANG').map(
(f) => `${f.key}=${envData[f.key] || ''}`
),
'',
];
fs.writeFileSync(userEnvPath, envLines.join('\n'), { encoding: 'utf-8' });
console.log(
'\x1b[32m%s\x1b[0m',
MSG[finishLang].CHANGE_ENV_DONE ||
MSG.CN.CHANGE_ENV_DONE ||
(finishLang === 'CN' ? '设置/修改已完成。' : 'Configuration updated.')
);
}
} catch (err) {
if (
err &&
(err.name === 'ExitPromptError' || err.message?.includes('SIGINT'))
) {
// 默认为中文友好提示,如需多语言可加参数
console.log('\x1b[33m操作已取消,环境变量更改已中止。\x1b[0m');
process.exit(0);
}
throw err;
}
}
module.exports = {
ensureUserEnvFileInteractive,
changeUserEnvFileInteractive,
getEnvFields,
};