Files
workbuddy_skills/oil-motion/scripts/credential-ui/src/cli.ts
T
admin 237a09a5b0 修:draw-ui / oil-motion 原被当子模块指针收录 ⇒ 改为正常文件入库(两份内容原先对别人是空的)
一、问题(本轮实测)
`draw-ui` 与 `oil-motion` 目录里**各自带一个内嵌 `.git`** ⇒ 上一次提交把它们记成了 **gitlink(子模块指针)**
⇒ 仓库里只存了一个不属于任何远端的 commit id,**别人克隆下来这两份是空的** ✗(`git status` 显示 ` m draw-ui` / ` m oil-motion` = 子模块内容有改动)。

二、处置(可回退)
· 把两处的 `.git` **挪走**(⛔ 不是删除)⇒ `归档/内嵌git-20261008/{draw-ui,oil-motion}.git`;
· `git rm --cached` 掉那两个 gitlink,再 `git add` 两个目录 ⇒ **按正常文件入库**(内容才真的进仓库)。

三、副作用(如实记)
挪走 `.git` 后,这两个技能**不能再原地 `git pull` 取上游更新**(要更新得重新拉一份覆盖);
如需恢复其本地仓库,把 `归档/内嵌git-20261008/` 里的 `.git` 挪回原处即可。
2026-10-08 22:29:52 +08:00

97 lines
6.3 KiB
TypeScript

import { randomBytes } from 'node:crypto';
import { lstat, mkdir, open, readFile, rename, unlink } from 'node:fs/promises';
import path from 'node:path';
import { fileURLToPath, pathToFileURL } from 'node:url';
import { loadManifest, nativeBackend, PublicError, validateManifest, type Manifest } from './config.ts';
import { loadPage, validateFields, validatePageUI } from './page.ts';
async function writeConfiguration(file: string, configuration: object) {
await mkdir(path.dirname(file), { recursive: true });
const temporary = file + '.' + randomBytes(8).toString('hex') + '.tmp';
try {
const handle = await open(temporary, 'wx', 0o600);
try { await handle.writeFile(JSON.stringify(configuration, null, 2) + '\n', 'utf8'); await handle.sync(); }
finally { await handle.close(); }
await rename(temporary, file);
} finally { await unlink(temporary).catch(() => {}); }
}
export async function configurePage(file: string, manifests: string[], options: Record<string, string>, dryRun = false) {
if (Object.keys(options).some(key => !['title', 'label', 'save-label'].includes(key))) throw new PublicError('存在不支持的页面配置项。');
let existing: { manifests: string[]; ui?: Record<string, string> } | undefined;
try {
const stat = await lstat(file);
if (!stat.isFile() || stat.isSymbolicLink() || stat.nlink > 1) throw new PublicError('页面配置必须是独立的普通文件。');
await loadPage(file);
existing = JSON.parse(await readFile(file, 'utf8'));
} catch (error) { if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw error; }
// 命令参数按当前目录解析;写入页面文件后统一保存为相对该文件的路径。
const paths = manifests.length ? manifests.map(item => path.relative(path.dirname(file), path.resolve(item)).split(path.sep).join('/')) : existing?.manifests ?? [];
validateFields(await Promise.all(paths.map(item => loadManifest(path.resolve(path.dirname(file), item)))));
const ui = { ...existing?.ui };
for (const [flag, key] of [['title', 'title'], ['label', 'label'], ['save-label', 'saveLabel']])
if (options[flag] !== undefined) ui[key] = options[flag];
const configuration = { version: 1, manifests: paths, ui: validatePageUI(ui) };
if (!dryRun) await writeConfiguration(file, configuration);
return { status: dryRun ? 'preview' : 'configured', page: file, configuration };
}
export async function configureManifest(file: string, options: Record<string, string>, dryRun = false) {
const allowed = ['id', 'label', 'credential', 'title', 'placeholder', 'save-label'];
if (Object.keys(options).some(k => !allowed.includes(k))) throw new PublicError('存在不支持的配置项。');
let existing: Manifest | undefined;
try {
const stat = await lstat(file);
if (!stat.isFile() || stat.isSymbolicLink() || stat.nlink > 1) throw new PublicError('声明必须是独立的普通文件。');
existing = await loadManifest(file);
} catch (error) { if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw error; }
if (existing && ((options.id && options.id !== existing.id) || (options.credential && options.credential !== existing.credential)))
throw new PublicError('已有声明的身份和凭据引用不可改写。请为新凭据指定新的 --manifest 文件。');
const draft: Record<string, unknown> = { version: 1, ...existing };
for (const key of ['id', 'label', 'credential']) if (options[key] !== undefined) draft[key] = options[key];
const ui = { ...existing?.ui };
for (const [flag, key] of [['title', 'title'], ['placeholder', 'placeholder'], ['save-label', 'saveLabel']] as const)
if (options[flag] !== undefined) ui[key] = options[flag];
if (Object.keys(ui).length) draft.ui = ui;
const manifest = validateManifest(draft);
if (!dryRun) await writeConfiguration(file, manifest);
return { status: dryRun ? 'preview' : 'configured', manifest: file, configuration: manifest };
}
async function main() {
const [command, ...args] = process.argv.slice(2);
if (!command || command === '--help') {
process.stdout.write('configure [--manifest 文件] --id 标识 --label 用途 --credential 引用 [--title 标题] [--placeholder 占位文字] [--save-label 按钮文字] [--dry-run]\nconfigure-page --page 文件 [--manifest 声明(可重复,替换整组)] [--title 标题] [--label 用途] [--save-label 按钮文字] [--dry-run]\nstatus [--manifest 文件]\n');
return;
}
const flags: Record<string, string> = {};
const manifests: string[] = [];
let dryRun = false;
for (let i = 0; i < args.length; i++) {
if (args[i] === '--dry-run' && ['configure', 'configure-page'].includes(command)) { dryRun = true; continue; }
if (command === 'configure-page' && args[i] === '--manifest' && args[i + 1] && !args[i + 1].startsWith('--')) { manifests.push(args[++i]); continue; }
const name = args[i].replace(/^--/, '');
if (!args[i].startsWith('--') || !args[i + 1] || args[i + 1].startsWith('--') || Object.hasOwn(flags, name))
throw new PublicError('命令参数不正确。');
flags[name] = args[++i];
}
if (command === 'configure-page') {
if (!flags.page) throw new PublicError('请指定 --page 配置文件。');
const file = path.resolve(flags.page); delete flags.page;
process.stdout.write(JSON.stringify(await configurePage(file, manifests, flags, dryRun)) + '\n'); return;
}
const file = path.resolve(flags.manifest ?? fileURLToPath(new URL('../manifests/default.json', import.meta.url)));
delete flags.manifest;
if (command === 'configure') process.stdout.write(JSON.stringify(await configureManifest(file, flags, dryRun)) + '\n');
else if (command === 'status' && !Object.keys(flags).length) {
const m = await loadManifest(file);
const backend = await nativeBackend(m.credential);
const configured = (await backend.get()) !== undefined;
process.stdout.write(JSON.stringify({ id: m.id, credential: m.credential, configured, storage: backend.name }) + '\n');
if (!configured) process.exitCode = 2;
} else throw new PublicError('命令不支持,请使用 --help。');
}
if (process.argv[1] && pathToFileURL(path.resolve(process.argv[1])).href === import.meta.url)
main().catch(error => {
process.stderr.write((error instanceof PublicError ? error.message : '配置操作未完成,请检查文件位置与权限。') + '\n');
process.exitCode = 1;
});