pczzz 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 pcoff contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,91 @@
1
+ # pcoff
2
+
3
+ 定时关机命令行工具
4
+
5
+ ## 安装
6
+
7
+ ```bash
8
+ npm install -g pcoff
9
+ ```
10
+
11
+ ## 使用
12
+
13
+ ### 命令行直接指定时间
14
+
15
+ ```bash
16
+ pcoff 1s # 1 秒后关机
17
+ pcoff 20m # 20 分钟后关机
18
+ pcoff 1h # 1 小时后关机
19
+ pcoff 1h30m # 组合单位:1 小时 30 分钟后
20
+ pcoff 90 # 纯数字按秒计
21
+ pcoff 30秒 # 中文单位也可以
22
+ ```
23
+
24
+ 支持的单位:`s` 秒 / `m` 分钟 / `h` 小时 / `d` 天(以及 sec、min、hour、秒、分钟、小时等写法),可以自由组合(如 `1h30m`)。
25
+
26
+ ### 交互式菜单
27
+
28
+ 直接运行 `pcoff`(不带参数):
29
+
30
+ ```
31
+ 你想多久关机?
32
+ 1. 30 秒
33
+ 2. 20 分钟
34
+ 3. 30 分钟
35
+ 4. 1 小时
36
+ 5. 2 小时
37
+ 6. 自定义
38
+ 7. 取消当前关机
39
+ 8. 退出
40
+ ```
41
+
42
+ 选择「自定义」后可继续输入任意时间(如 `45m`、`1h20m`、`300`)。
43
+
44
+ ### 取消关机
45
+
46
+ ```bash
47
+ pcoff cancel
48
+ ```
49
+
50
+ 或在菜单中选择「取消当前关机」。
51
+
52
+ 已安排的关机由操作系统执行,退出程序、关闭窗口都不影响;只有显式取消才会生效。
53
+
54
+ ## 重复安排检测
55
+
56
+ 安排新的关机前,pcoff 会检查是否已存在定时关机任务:
57
+
58
+ 1. **自己的记录**(`~/.pcoff/task.json`):能显示确切的剩余时间,例如
59
+ 「当前已安排 1 小时 29 分钟 后关机(本工具于 23:01:35 设置)。要重新安排吗?(Y/N)」
60
+ 2. **系统级检测(仅 Windows)**:即使任务不是本工具设置的(比如手动执行过 `shutdown -s -t 3600`),也能检测到。Windows 在已有计划关机时再次执行 `shutdown` 会返回错误码 1190,据此弹出「要取消旧任务并重新安排吗?」的询问。外来任务拿不到剩余时间,只能提示存在。
61
+
62
+ 注意:电脑重启后 Windows 的关机计时器会消失。此时记录文件若还在,重新安排时系统探测会发现"其实没有旧任务",直接按新时间安排,记录随之更新,不会误伤。
63
+
64
+ ## 平台差异
65
+
66
+ | 平台 | 精度 | 权限 | 说明 |
67
+ | ------------- | ------ | ----------------------------- | ----------------------------------------------------- |
68
+ | Windows | 秒级 | 无需管理员 | 完整支持,含系统级检测 |
69
+ | macOS / Linux | 分钟级 | 需要 root(`sudo pcoff ...`) | `shutdown` 命令仅支持分钟;无系统级检测,依赖记录文件 |
70
+
71
+ ## 开发
72
+
73
+ ```bash
74
+ npm install
75
+ npm run build # tsc 编译到 dist/
76
+ npm test # 冒烟测试(PCOFF_DRY_RUN=1,不会真的关机)
77
+ npm link # 本地全局试用 pcoff 命令
78
+ ```
79
+
80
+ 测试钩子:`PCOFF_DRY_RUN=1` 跳过真实的 shutdown 调用;`PCOFF_HOME` 指定记录文件目录(默认 `~/.pcoff`)。
81
+
82
+ ## 发布
83
+
84
+ ```bash
85
+ npm login
86
+ npm publish
87
+ ```
88
+
89
+ ## License
90
+
91
+ MIT
package/dist/cli.js ADDED
@@ -0,0 +1,204 @@
1
+ #!/usr/bin/env node
2
+ import { readFileSync } from 'node:fs';
3
+ import { MAX_SECONDS, formatClock, formatDuration, formatTimeOfDay, parseDuration } from './time.js';
4
+ import { clearTask, loadActiveTask, saveTask } from './store.js';
5
+ import { abortShutdown, scheduleShutdown } from './shutdown.js';
6
+ import { Prompt } from './menu.js';
7
+ import { bold, cyan, dim, green, red, yellow } from './ui.js';
8
+ const { version: VERSION } = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8'));
9
+ const HELP = `pcoff — 定时关机小工具
10
+
11
+ 用法:
12
+ pcoff 打开交互式菜单
13
+ pcoff <时间> 指定时间后关机,例如: pcoff 30s
14
+ pcoff cancel 取消当前已安排的定时关机
15
+
16
+ 时间格式(可自由组合):
17
+ 30s 30 秒后 20m 20 分钟后 1h 1 小时后
18
+ 1h30m 1 小时 30 分钟后 1d12h 也支持天 90 纯数字按秒计
19
+ 中文单位同样有效: 30秒 / 20分钟 / 1小时30分
20
+
21
+ 选项:
22
+ -h, --help 显示本帮助
23
+ -v, --version 显示版本号
24
+
25
+ 说明:
26
+ · 关机计划由操作系统执行,退出本程序或关闭窗口不会取消它。
27
+ · 取消方式: pcoff cancel,或菜单中的「取消当前关机」。
28
+ · Windows 完整支持(秒级精度,无需管理员权限)。
29
+ · macOS / Linux: shutdown 需要 root 权限(sudo pcoff ...),且只能按分钟安排。`;
30
+ function fail(message) {
31
+ console.error(message);
32
+ process.exit(1);
33
+ }
34
+ function exitGracefully() {
35
+ process.stdout.write('\n已退出。已设置的定时关机仍会执行;取消请运行 pcoff cancel。\n');
36
+ process.exit(0);
37
+ }
38
+ /** 安排成功后的提示 + 倒计时。倒计时期间 Ctrl+C 随时退出,不影响系统层面的关机。 */
39
+ async function printScheduled(seconds) {
40
+ const fireAt = new Date(Date.now() + seconds * 1000);
41
+ try {
42
+ await saveTask(seconds);
43
+ }
44
+ catch (err) {
45
+ console.log(yellow(`(记录文件写入失败,不影响已安排的关机:${err instanceof Error ? err.message : String(err)})`));
46
+ }
47
+ console.log(green(`✅ 已安排在本机 ${formatTimeOfDay(fireAt)} 自动关机(${formatDuration(seconds)} 后)。`));
48
+ console.log(dim(' 关机由系统执行,关闭本窗口不影响;取消请运行 pcoff cancel。'));
49
+ startCountdown(seconds);
50
+ }
51
+ function startCountdown(seconds) {
52
+ if (!process.stdout.isTTY)
53
+ return;
54
+ const fireAt = Date.now() + seconds * 1000;
55
+ const timer = setInterval(() => {
56
+ const left = Math.ceil((fireAt - Date.now()) / 1000);
57
+ if (left <= 0) {
58
+ clearInterval(timer);
59
+ process.stdout.write('\r' + ' '.repeat(80) + '\r');
60
+ console.log(green('时间到,系统正在关机。'));
61
+ process.exit(0);
62
+ }
63
+ process.stdout.write(`\r${dim(`剩余 ${formatClock(left)},Ctrl+C 退出(不影响关机)`)}`);
64
+ }, 1000);
65
+ }
66
+ /**
67
+ * 带检测的安排流程。返回 true 表示新计划已生效(此后由倒计时接管),
68
+ * 返回 false 表示用户选择保留旧计划。
69
+ *
70
+ * 检测分两层:
71
+ * 1. 记录文件(~/.pcoff/task.json):本工具自己设置的、尚未到期的任务,能给出剩余时间;
72
+ * 2. Windows 系统探测:shutdown 若因错误码 1190 失败,说明系统里另有一个计划中的关机
73
+ * (外来任务或重启后记录失效),此时询问用户是否取消旧的再排新的。
74
+ */
75
+ async function scheduleWithCheck(prompt, seconds) {
76
+ const own = await loadActiveTask();
77
+ if (own) {
78
+ const remaining = Math.ceil((Date.parse(own.fireAt) - Date.now()) / 1000);
79
+ const replace = await prompt.confirm(`当前已安排 ${bold(formatDuration(remaining))} 后关机(本工具于 ${formatTimeOfDay(new Date(own.createdAt))} 设置)。要重新安排吗?`);
80
+ if (!replace) {
81
+ console.log(dim('已保留原计划;取消请运行 pcoff cancel。'));
82
+ return false;
83
+ }
84
+ }
85
+ let res = await scheduleShutdown(seconds);
86
+ if (res.ok) {
87
+ await printScheduled(seconds);
88
+ return true;
89
+ }
90
+ if (res.alreadyScheduled) {
91
+ if (!own) {
92
+ const replace = await prompt.confirm('检测到系统已有一个定时关机任务(可能由其他方式设置,剩余时间未知)。要取消它并按新时间安排吗?');
93
+ if (!replace) {
94
+ console.log(dim('已保留现有计划;取消请运行 pcoff cancel。'));
95
+ return false;
96
+ }
97
+ }
98
+ // 用户已确认替换(或旧任务就是本工具设的),先取消系统里的旧任务再排新的
99
+ await abortShutdown();
100
+ res = await scheduleShutdown(seconds);
101
+ if (res.ok) {
102
+ await printScheduled(seconds);
103
+ return true;
104
+ }
105
+ }
106
+ let message = red(`安排关机失败:${res.stderr || '未知错误'}`);
107
+ if (process.platform !== 'win32' && /root|permission|permitt|not allowed/i.test(res.stderr)) {
108
+ message += dim('\n(macOS/Linux 的 shutdown 需要 root 权限,可尝试:sudo pcoff ...)');
109
+ }
110
+ fail(message);
111
+ }
112
+ async function runCancel() {
113
+ const res = await abortShutdown();
114
+ await clearTask();
115
+ if (res.ok) {
116
+ console.log(green('✅ 已取消当前定时关机。'));
117
+ }
118
+ else {
119
+ console.log(yellow('没有需要取消的定时关机(可能不存在、电脑已重启或已执行)。'));
120
+ if (res.stderr)
121
+ console.log(dim(` 系统返回:${res.stderr}`));
122
+ }
123
+ }
124
+ async function menuLoop(prompt) {
125
+ for (;;) {
126
+ console.log(`\n${bold('你想多久关机?')}`);
127
+ ['30 秒', '20 分钟', '30 分钟', '1 小时', '2 小时', '自定义', '取消当前关机', '退出'].forEach((label, i) => console.log(` ${cyan(String(i + 1))}. ${label}`));
128
+ const choice = await prompt.choose(`请选择 ${dim('[1-8]')}: `, 1, 8);
129
+ switch (choice) {
130
+ case 1:
131
+ if (await scheduleWithCheck(prompt, 30))
132
+ return;
133
+ break;
134
+ case 2:
135
+ if (await scheduleWithCheck(prompt, 20 * 60))
136
+ return;
137
+ break;
138
+ case 3:
139
+ if (await scheduleWithCheck(prompt, 30 * 60))
140
+ return;
141
+ break;
142
+ case 4:
143
+ if (await scheduleWithCheck(prompt, 60 * 60))
144
+ return;
145
+ break;
146
+ case 5:
147
+ if (await scheduleWithCheck(prompt, 2 * 60 * 60))
148
+ return;
149
+ break;
150
+ case 6:
151
+ if (await scheduleWithCheck(prompt, await prompt.askDuration()))
152
+ return;
153
+ break;
154
+ case 7:
155
+ await runCancel();
156
+ break;
157
+ case 8:
158
+ console.log(dim('再见 👋'));
159
+ return;
160
+ }
161
+ }
162
+ }
163
+ async function main() {
164
+ const argv = process.argv.slice(2);
165
+ if (argv.includes('-h') || argv.includes('--help')) {
166
+ console.log(HELP);
167
+ return;
168
+ }
169
+ if (argv.includes('-v') || argv.includes('--version')) {
170
+ console.log(`pcoff v${VERSION}`);
171
+ return;
172
+ }
173
+ if (argv.includes('cancel') || argv.includes('-c')) {
174
+ await runCancel();
175
+ return;
176
+ }
177
+ const prompt = new Prompt();
178
+ try {
179
+ if (argv.length === 0) {
180
+ await menuLoop(prompt);
181
+ return;
182
+ }
183
+ if (argv.length > 1) {
184
+ fail(red('参数太多,一次只接受一个时间。') + dim('\n用法:pcoff 30s / 20m / 1h;不带参数打开菜单;pcoff cancel 取消。'));
185
+ }
186
+ const parsed = parseDuration(argv[0] ?? '');
187
+ if (!parsed) {
188
+ fail(red(`无法识别的时间:“${argv[0] ?? ''}”`) + dim('\n示例:30s / 20m / 1h / 1h30m;纯数字按秒计。详见 pcoff -h。'));
189
+ }
190
+ if (parsed.totalSeconds < 1)
191
+ fail(red('时间至少 1 秒。'));
192
+ if (parsed.totalSeconds > MAX_SECONDS)
193
+ fail(red('时间最长 10 年。'));
194
+ await scheduleWithCheck(prompt, parsed.totalSeconds);
195
+ }
196
+ finally {
197
+ prompt.close();
198
+ }
199
+ }
200
+ process.on('SIGINT', exitGracefully);
201
+ main().catch((err) => {
202
+ console.error(red(`出错了:${err instanceof Error ? err.message : String(err)}`));
203
+ process.exit(1);
204
+ });
package/dist/menu.js ADDED
@@ -0,0 +1,88 @@
1
+ import readline from 'node:readline/promises';
2
+ import { stdin, stdout } from 'node:process';
3
+ import { MAX_SECONDS, parseDuration } from './time.js';
4
+ import { cyan, red } from './ui.js';
5
+ /** 交互式问答封装。零依赖,全部基于 node:readline。 */
6
+ export class Prompt {
7
+ rl = readline.createInterface({ input: stdin, output: stdout });
8
+ buffered = [];
9
+ waiter = null;
10
+ closed = false;
11
+ constructor() {
12
+ // 输入可能先于下一次提问到达(例如管道喂入多行),必须排队,否则中间的行会被丢掉
13
+ this.rl.on('line', (line) => {
14
+ const waiter = this.waiter;
15
+ if (waiter) {
16
+ this.waiter = null;
17
+ waiter.resolve(line);
18
+ }
19
+ else {
20
+ this.buffered.push(line);
21
+ }
22
+ });
23
+ this.rl.on('close', () => {
24
+ this.closed = true;
25
+ const waiter = this.waiter;
26
+ if (waiter) {
27
+ this.waiter = null;
28
+ waiter.reject(new Error('输入流已结束'));
29
+ }
30
+ });
31
+ // readline 处于原始模式时 Ctrl+C 只会触发 'SIGINT' 事件,不会杀掉进程,需要自己接住
32
+ this.rl.on('SIGINT', () => {
33
+ stdout.write('\n已退出。已设置的定时关机仍会执行;取消请运行 pcoff cancel。\n');
34
+ process.exit(0);
35
+ });
36
+ }
37
+ readLine() {
38
+ const buffered = this.buffered.shift();
39
+ if (buffered !== undefined)
40
+ return Promise.resolve(buffered);
41
+ if (this.closed)
42
+ return Promise.reject(new Error('输入流已结束'));
43
+ return new Promise((resolve, reject) => {
44
+ this.waiter = { resolve, reject };
45
+ });
46
+ }
47
+ async choose(question, min, max) {
48
+ for (;;) {
49
+ stdout.write(question);
50
+ const answer = (await this.readLine()).trim();
51
+ const value = Number(answer);
52
+ if (Number.isInteger(value) && value >= min && value <= max)
53
+ return value;
54
+ stdout.write(red(`请输入 ${min} ~ ${max} 之间的数字\n`));
55
+ }
56
+ }
57
+ async confirm(question) {
58
+ for (;;) {
59
+ stdout.write(`${question} ${cyan('(Y/N) ')}`);
60
+ const answer = (await this.readLine()).trim().toLowerCase();
61
+ if (answer === 'y' || answer === 'yes')
62
+ return true;
63
+ if (answer === 'n' || answer === 'no')
64
+ return false;
65
+ stdout.write(red('请输入 Y 或 N\n'));
66
+ }
67
+ }
68
+ async askDuration() {
69
+ for (;;) {
70
+ stdout.write(cyan('请输入时间(如 30s / 20m / 1h / 1h30m,纯数字按秒计): '));
71
+ const answer = await this.readLine();
72
+ const parsed = parseDuration(answer);
73
+ if (!parsed || parsed.totalSeconds < 1) {
74
+ stdout.write(red('看不懂这个时间,请参考示例:30s、20m、1h、1h30m\n'));
75
+ continue;
76
+ }
77
+ if (parsed.totalSeconds > MAX_SECONDS) {
78
+ stdout.write(red('时间最长 10 年。\n'));
79
+ continue;
80
+ }
81
+ return parsed.totalSeconds;
82
+ }
83
+ }
84
+ close() {
85
+ this.closed = true;
86
+ this.rl.close();
87
+ }
88
+ }
@@ -0,0 +1,70 @@
1
+ import { execFile } from 'node:child_process';
2
+ import { promisify } from 'node:util';
3
+ const exec = promisify(execFile);
4
+ /**
5
+ * Windows:系统已有计划中的关机任务(ERROR_SHUTDOWN_IS_SCHEDULED)。
6
+ * shutdown.exe 把 Win32 错误码作为进程退出码,因此可以直接用退出码判断。
7
+ */
8
+ const WERR_SHUTDOWN_IS_SCHEDULED = 1190;
9
+ /** 中文 Windows 的控制台输出是 GBK 系编码,按 UTF-8 解会得到乱码 */
10
+ function decodeOutput(data) {
11
+ if (!data)
12
+ return '';
13
+ if (typeof data === 'string')
14
+ return data.trim();
15
+ if (process.platform === 'win32') {
16
+ try {
17
+ return new TextDecoder('gb18030').decode(data).trim();
18
+ }
19
+ catch {
20
+ return data.toString('utf8').trim();
21
+ }
22
+ }
23
+ return data.toString('utf8').trim();
24
+ }
25
+ function describe(err) {
26
+ const e = err;
27
+ const stderr = decodeOutput(e?.stderr) || (e?.message ?? String(err ?? ''));
28
+ return { code: e?.code, stderr };
29
+ }
30
+ function isAlreadyScheduled(code, stderr) {
31
+ return Number(code) === WERR_SHUTDOWN_IS_SCHEDULED || stderr.includes('1190');
32
+ }
33
+ export async function scheduleShutdown(seconds) {
34
+ if (process.env.PCOFF_DRY_RUN === '1') {
35
+ return { ok: true, alreadyScheduled: false, stderr: '' };
36
+ }
37
+ try {
38
+ if (process.platform === 'win32') {
39
+ await exec('shutdown', ['/s', '/t', String(Math.round(seconds)), '/c', 'pcoff 定时关机'], {
40
+ windowsHide: true,
41
+ encoding: 'buffer',
42
+ });
43
+ }
44
+ else {
45
+ // Linux/macOS 的 shutdown 只支持分钟粒度;均需要 root 权限
46
+ const minutes = Math.max(1, Math.ceil(seconds / 60));
47
+ await exec('shutdown', ['-h', `+${minutes}`]);
48
+ }
49
+ return { ok: true, alreadyScheduled: false, stderr: '' };
50
+ }
51
+ catch (err) {
52
+ const { code, stderr } = describe(err);
53
+ return { ok: false, alreadyScheduled: isAlreadyScheduled(code, stderr), stderr };
54
+ }
55
+ }
56
+ export async function abortShutdown() {
57
+ if (process.env.PCOFF_DRY_RUN === '1') {
58
+ return { ok: true, nothingPending: false, stderr: '' };
59
+ }
60
+ const args = process.platform === 'win32' ? ['/a'] : ['-c'];
61
+ try {
62
+ await exec('shutdown', args, { windowsHide: true, encoding: 'buffer' });
63
+ return { ok: true, nothingPending: false, stderr: '' };
64
+ }
65
+ catch (err) {
66
+ // 失败的最常见原因就是"本来就没有计划中的关机"
67
+ const { stderr } = describe(err);
68
+ return { ok: false, nothingPending: true, stderr };
69
+ }
70
+ }
package/dist/store.js ADDED
@@ -0,0 +1,53 @@
1
+ import { promises as fs } from 'node:fs';
2
+ import os from 'node:os';
3
+ import path from 'node:path';
4
+ function taskFile() {
5
+ const base = process.env.PCOFF_HOME ?? path.join(os.homedir(), '.pcoff');
6
+ return path.join(base, 'task.json');
7
+ }
8
+ /**
9
+ * 读取本工具上一次安排且尚未到期的关机记录。
10
+ * 过期或损坏的记录会被顺手清掉;读不到返回 null。
11
+ */
12
+ export async function loadActiveTask() {
13
+ let text;
14
+ try {
15
+ text = await fs.readFile(taskFile(), 'utf8');
16
+ }
17
+ catch {
18
+ return null;
19
+ }
20
+ let task;
21
+ try {
22
+ task = JSON.parse(text);
23
+ }
24
+ catch {
25
+ await clearTask();
26
+ return null;
27
+ }
28
+ const fireAt = new Date(task.fireAt);
29
+ if (Number.isNaN(fireAt.getTime()) || fireAt.getTime() <= Date.now()) {
30
+ await clearTask();
31
+ return null;
32
+ }
33
+ return task;
34
+ }
35
+ export async function saveTask(seconds) {
36
+ const now = Date.now();
37
+ const task = {
38
+ createdAt: new Date(now).toISOString(),
39
+ fireAt: new Date(now + seconds * 1000).toISOString(),
40
+ seconds,
41
+ platform: process.platform,
42
+ };
43
+ await fs.mkdir(path.dirname(taskFile()), { recursive: true });
44
+ await fs.writeFile(taskFile(), JSON.stringify(task, null, 2) + '\n', 'utf8');
45
+ }
46
+ export async function clearTask() {
47
+ try {
48
+ await fs.rm(taskFile(), { force: true });
49
+ }
50
+ catch {
51
+ // 记录文件删不掉不该影响主流程
52
+ }
53
+ }
package/dist/time.js ADDED
@@ -0,0 +1,90 @@
1
+ const UNIT_SECONDS = {
2
+ s: 1,
3
+ m: 60,
4
+ h: 3600,
5
+ d: 86400,
6
+ };
7
+ // 同前缀的长单位要排在短单位之前,否则 "min" 会被拆成 "m" + "in"
8
+ const UNIT_ALIASES = [
9
+ ['小时', 'h'],
10
+ ['hours', 'h'], ['hour', 'h'], ['hrs', 'h'], ['hr', 'h'], ['h', 'h'],
11
+ ['天', 'd'], ['days', 'd'], ['day', 'd'], ['d', 'd'],
12
+ ['分钟', 'm'], ['分', 'm'],
13
+ ['minutes', 'm'], ['mins', 'm'], ['min', 'm'], ['m', 'm'],
14
+ ['秒钟', 's'], ['秒', 's'],
15
+ ['seconds', 's'], ['secs', 's'], ['sec', 's'], ['s', 's'],
16
+ ];
17
+ /** Windows shutdown /t 的上限:10 年 */
18
+ export const MAX_SECONDS = 315_360_000;
19
+ /**
20
+ * 解析 "1s" / "20m" / "1h30m" / "90" / "2分30秒" 这类时间表达。
21
+ * 纯数字按秒处理;无法完整识别时返回 null。
22
+ */
23
+ export function parseDuration(input) {
24
+ const raw = input.trim().toLowerCase().replace(/\s+/g, '');
25
+ if (!raw)
26
+ return null;
27
+ if (/^\d+(?:\.\d+)?$/.test(raw)) {
28
+ const value = Number(raw);
29
+ if (!Number.isFinite(value))
30
+ return null;
31
+ return { totalSeconds: Math.round(value), segments: [{ value, unit: 's' }] };
32
+ }
33
+ const segments = [];
34
+ let rest = raw;
35
+ while (rest.length > 0) {
36
+ const matched = /^(\d+(?:\.\d+)?)(.*)$/.exec(rest);
37
+ if (!matched)
38
+ return null;
39
+ const value = Number(matched[1]);
40
+ const unitPart = matched[2] ?? '';
41
+ const alias = UNIT_ALIASES.find(([name]) => unitPart.startsWith(name));
42
+ if (!alias || !Number.isFinite(value))
43
+ return null;
44
+ segments.push({ value, unit: alias[1] });
45
+ rest = unitPart.slice(alias[0].length);
46
+ }
47
+ let totalSeconds = 0;
48
+ for (const seg of segments) {
49
+ totalSeconds += seg.value * UNIT_SECONDS[seg.unit];
50
+ }
51
+ return { totalSeconds: Math.round(totalSeconds), segments };
52
+ }
53
+ /** 90 -> "1 分钟 30 秒";0 -> "0 秒" */
54
+ export function formatDuration(totalSeconds) {
55
+ let s = Math.max(0, Math.round(totalSeconds));
56
+ const d = Math.floor(s / 86400);
57
+ s -= d * 86400;
58
+ const h = Math.floor(s / 3600);
59
+ s -= h * 3600;
60
+ const m = Math.floor(s / 60);
61
+ s -= m * 60;
62
+ const parts = [];
63
+ if (d > 0)
64
+ parts.push(`${d} 天`);
65
+ if (h > 0)
66
+ parts.push(`${h} 小时`);
67
+ if (m > 0)
68
+ parts.push(`${m} 分钟`);
69
+ if (s > 0 || parts.length === 0)
70
+ parts.push(`${s} 秒`);
71
+ return parts.join(' ');
72
+ }
73
+ /** 倒计时用:3661 -> "1:01:01",59 -> "00:59" */
74
+ export function formatClock(totalSeconds) {
75
+ const s = Math.max(0, Math.round(totalSeconds));
76
+ const h = Math.floor(s / 3600);
77
+ const m = Math.floor((s % 3600) / 60);
78
+ const sec = s % 60;
79
+ const mm = String(m).padStart(2, '0');
80
+ const ss = String(sec).padStart(2, '0');
81
+ return h > 0 ? `${h}:${mm}:${ss}` : `${mm}:${ss}`;
82
+ }
83
+ export function formatTimeOfDay(date) {
84
+ return date.toLocaleTimeString('zh-CN', {
85
+ hour12: false,
86
+ hour: '2-digit',
87
+ minute: '2-digit',
88
+ second: '2-digit',
89
+ });
90
+ }
package/dist/ui.js ADDED
@@ -0,0 +1,10 @@
1
+ const useColor = process.stdout.isTTY === true && !process.env.NO_COLOR;
2
+ function color(code, text) {
3
+ return useColor ? `\x1b[${code}m${text}\x1b[0m` : text;
4
+ }
5
+ export const bold = (text) => color('1', text);
6
+ export const dim = (text) => color('2', text);
7
+ export const red = (text) => color('31', text);
8
+ export const green = (text) => color('32', text);
9
+ export const yellow = (text) => color('33', text);
10
+ export const cyan = (text) => color('36', text);
package/package.json ADDED
@@ -0,0 +1,32 @@
1
+ {
2
+ "name": "pczzz",
3
+ "version": "0.0.1",
4
+ "description": "Timed shutdown command tool",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "bin": {
8
+ "pczzz": "dist/cli.js"
9
+ },
10
+ "files": [
11
+ "dist"
12
+ ],
13
+ "engines": {
14
+ "node": ">=20"
15
+ },
16
+ "keywords": [
17
+ "shutdown",
18
+ "poweroff",
19
+ "timed shutdown",
20
+ "windows",
21
+ "pczzz"
22
+ ],
23
+ "scripts": {
24
+ "build": "tsc",
25
+ "test": "node test/smoke.mjs",
26
+ "prepublishOnly": "npm run build && npm test"
27
+ },
28
+ "devDependencies": {
29
+ "@types/node": "^22.0.0",
30
+ "typescript": "^5.6.0"
31
+ }
32
+ }