onepage-cli 0.0.0-stage → 1.0.0

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/README.md CHANGED
@@ -1,3 +1,98 @@
1
- # Temporary Holding Version
1
+ # OnePage CLI(onepage-cli)
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ 在终端用 git 风格指令生成、部署、管理你的 OnePage 静态网页。零依赖,Node 18+ 即可运行。
4
+
5
+ ## 安装
6
+
7
+ ```bash
8
+ npm install -g onepage-cli
9
+ ```
10
+
11
+ 安装后即可使用 `op` 命令(注意:若你同时安装了 1Password CLI,其命令也是 `op`,可通过 `npm exec -p onepage-cli -- op ...` 或全局别名区分)。
12
+
13
+ ## 快速开始
14
+
15
+ ```bash
16
+ op login # 登录(输入邮箱 + 密码)
17
+ op init # 在项目目录创建暂存区
18
+ op add . # 把代码加入暂存区
19
+ op push # 部署上线(交互式输入短链名、预览图、公开、SEO)
20
+ ```
21
+
22
+ ## 全部命令
23
+
24
+ ### 账号
25
+
26
+ | 命令 | 说明 |
27
+ |---|---|
28
+ | `op login` | 登录。新邮箱**自动注册**,用户名默认为邮箱 `@` 前面的字符(重名自动加随机后缀) |
29
+ | `op whoami` | 查看当前登录账号 |
30
+ | `op logout` | 退出登录(清除本地凭证) |
31
+
32
+ ### 部署流程
33
+
34
+ | 命令 | 说明 |
35
+ |---|---|
36
+ | `op init` | 在当前目录创建代码暂存区 `.onepage/` |
37
+ | `op add [路径]` | 把文件/目录加入暂存区,默认当前目录 `.`(自动过滤不支持的类型与 node_modules 等) |
38
+ | `op status` | 查看暂存区文件清单与大小 |
39
+ | `op push` | 部署暂存区代码上线,依次交互确认:**短链名** → **预览图路径或 no** → **是否公开 yes/no** → **是否 SEO 优化 yes/no**。短链名已存在且属于自己时为**覆盖更新** |
40
+
41
+ ### 站点管理
42
+
43
+ | 命令 | 说明 |
44
+ |---|---|
45
+ | `op list` | 表格列出本账号所有站点(短链名/标题/公开/浏览/点赞/更新时间/URL) |
46
+ | `op clone <url>` | 拉取站点代码到 `./<短链名>/`(支持完整 URL 或短链名;公开站点可拉,私有站点仅所有者) |
47
+ | `op delete <短链名>` | 删除指定站点(需二次确认 yes/no) |
48
+ | `op open <短链名>` | 在浏览器打开站点 |
49
+
50
+ ## 支持的文件类型
51
+
52
+ `html` `css` `js` `mjs` `jpg` `jpeg` `png` `gif` `svg` `webp` `ico` `json` `txt` `md` `woff` `woff2` `ttf` `eot` `otf` `mp3` `mp4` `webm` `ogg` `wav` `map`
53
+
54
+ 暂存区自动跳过:`.onepage`、`node_modules`、`.git` 及隐藏文件。
55
+
56
+ ## 配置
57
+
58
+ 登录凭证保存在 `~/.onepage-cli.json`(权限 600):
59
+
60
+ ```json
61
+ {
62
+ "server": "https://app1.cloudstudio.site",
63
+ "token": "...",
64
+ "username": "...",
65
+ "email": "..."
66
+ }
67
+ ```
68
+
69
+ ## 典型工作流
70
+
71
+ ```bash
72
+ # 首次
73
+ op login
74
+ mkdir my-site && cd my-site
75
+ # ... 编写 index.html 等文件 ...
76
+ op init && op add . && op push
77
+ # → 短链名 [my-site]:
78
+ # → 预览图路径(no 跳过): no
79
+ # → 是否公开(yes/no): yes
80
+ # → 是否 SEO 优化(yes/no): yes
81
+ # ✓ 部署完成:https://app1.cloudstudio.site/my-site/
82
+
83
+ # 更新:改完代码再 add + push(短链名不变即覆盖更新)
84
+ op add . && op push
85
+
86
+ # 备份/迁移
87
+ op clone https://app1.cloudstudio.site/my-site/
88
+ ```
89
+
90
+ ## 安全说明
91
+
92
+ - 密码仅在登录/注册时通过 HTTPS 提交一次,本地只保存服务端签发的 token
93
+ - token 与账号绑定,`op logout` 即可作废本地凭证
94
+ - 站点文件由服务端校验扩展名与相对路径(禁止 `..` 越权)
95
+
96
+ ## License
97
+
98
+ MIT
package/bin/op.js ADDED
@@ -0,0 +1,377 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+ // OnePage CLI —— git 风格的静态网页部署命令行工具
4
+ // 文档:https://app1.cloudstudio.site
5
+ const fs = require('fs');
6
+ const path = require('path');
7
+ const os = require('os');
8
+ const { exec } = require('child_process');
9
+ const conf = require('../lib/config');
10
+ const { api } = require('../lib/api');
11
+ const { ask, askHidden, parseBool } = require('../lib/prompt');
12
+ const { ok, err, info, warn, table, pad } = require('../lib/util');
13
+
14
+ const STAGE_DIR = '.onepage';
15
+ const STAGE_FILES = path.join(STAGE_DIR, 'stage');
16
+ const STAGE_MANIFEST = path.join(STAGE_DIR, 'manifest.json');
17
+
18
+ // 允许部署的文件扩展名(与服务端保持一致)
19
+ const ALLOWED_EXTS = new Set([
20
+ 'html', 'css', 'js', 'mjs', 'jpg', 'jpeg', 'png', 'gif', 'svg', 'webp', 'ico',
21
+ 'json', 'txt', 'md', 'woff', 'woff2', 'ttf', 'eot', 'otf',
22
+ 'mp3', 'mp4', 'webm', 'ogg', 'wav', 'map',
23
+ ]);
24
+ const SKIP_DIRS = new Set([STAGE_DIR, 'node_modules', '.git', '.svn']);
25
+
26
+ const IS_PUBLIC = (process.stdout.isTTY !== false) && process.platform !== 'win32'
27
+ ? true : true; // ANSI 在多数终端可用
28
+
29
+ function die(msg) { err(msg); process.exit(1); }
30
+
31
+ function requireToken(cfg) {
32
+ if (!cfg.token) die('尚未登录,请先执行 op login');
33
+ }
34
+
35
+ async function requireAuth(cfg) {
36
+ requireToken(cfg);
37
+ const r = await api(cfg, 'GET', '/api/cli/sites');
38
+ if (r.status === 401) die('登录已失效,请重新执行 op login');
39
+ return r;
40
+ }
41
+
42
+ /* ---------- login ---------- */
43
+ async function cmdLogin() {
44
+ const cfg = conf.load();
45
+ console.log('登录 OnePage(新邮箱将自动注册,用户名默认为邮箱 @ 前缀)');
46
+ const email = await ask('邮箱');
47
+ if (!/^[^@\s]+@[^@\s]+\.[^@\s]+$/.test(email)) die('邮箱格式不正确');
48
+ const password = await askHidden('密码');
49
+ if (password.length < 6) die('密码至少 6 位');
50
+
51
+ info('登录中…');
52
+ const r = await api(cfg, 'POST', '/api/cli/login', { email, password });
53
+ if (!r.data.ok) die(r.data.error || '登录失败');
54
+
55
+ cfg.token = r.data.token;
56
+ cfg.username = r.data.username;
57
+ cfg.email = r.data.email;
58
+ conf.save(cfg);
59
+ if (r.data.created) {
60
+ ok('新账号已自动创建并登录:' + r.data.username + ' <' + r.data.email + '>');
61
+ } else {
62
+ ok('登录成功:' + r.data.username);
63
+ }
64
+ console.log('配置已保存到 ' + conf.CONF_PATH);
65
+ }
66
+
67
+ /* ---------- whoami ---------- */
68
+ function cmdWhoami() {
69
+ const cfg = conf.load();
70
+ if (!cfg.token) { warn('未登录'); return; }
71
+ table(['字段', '值'], [
72
+ ['用户名', cfg.username || '-'],
73
+ ['邮箱', cfg.email || '-'],
74
+ ['服务器', cfg.server],
75
+ ]);
76
+ }
77
+
78
+ /* ---------- logout ---------- */
79
+ function cmdLogout() {
80
+ const cfg = conf.load();
81
+ cfg.token = ''; cfg.username = ''; cfg.email = '';
82
+ conf.save(cfg);
83
+ ok('已退出登录(本地凭证已清除)');
84
+ }
85
+
86
+ /* ---------- init ---------- */
87
+ function cmdInit() {
88
+ const cwd = process.cwd();
89
+ if (fs.existsSync(STAGE_DIR)) {
90
+ warn('当前目录已存在暂存区(.onepage/)');
91
+ return;
92
+ }
93
+ fs.mkdirSync(STAGE_FILES, { recursive: true });
94
+ fs.writeFileSync(STAGE_MANIFEST, JSON.stringify({
95
+ version: 1,
96
+ created_at: new Date().toISOString(),
97
+ files: [],
98
+ }, null, 2));
99
+ ok('已创建暂存区 .onepage/stage/');
100
+ console.log('接下来:');
101
+ console.log(' ' + 'op add .' + ' ' + '把当前目录代码加入暂存区');
102
+ console.log(' ' + 'op push' + ' ' + '把暂存区代码部署上线');
103
+ }
104
+
105
+ /* ---------- add ---------- */
106
+ function walkFiles(dir, base, out) {
107
+ for (const name of fs.readdirSync(dir)) {
108
+ if (name.startsWith('.')) continue; // 跳过隐藏文件/目录
109
+ const full = path.join(dir, name);
110
+ const rel = path.relative(base, full).split(path.sep).join('/');
111
+ const st = fs.statSync(full);
112
+ if (st.isDirectory()) {
113
+ if (SKIP_DIRS.has(name)) continue;
114
+ walkFiles(full, base, out);
115
+ } else {
116
+ const ext = name.includes('.') ? name.split('.').pop().toLowerCase() : '';
117
+ if (!ALLOWED_EXTS.has(ext)) continue;
118
+ out.push({ rel, full });
119
+ }
120
+ }
121
+ }
122
+
123
+ function cmdAdd(args) {
124
+ if (!fs.existsSync(STAGE_DIR)) die('暂存区不存在,请先执行 op init');
125
+ const targets = args.length ? args : ['.'];
126
+ const cwd = process.cwd();
127
+ const added = [];
128
+ const skipped = [];
129
+
130
+ for (const t of targets) {
131
+ const p = path.resolve(cwd, t);
132
+ if (!fs.existsSync(p)) { err('路径不存在:' + t); continue; }
133
+ const st = fs.statSync(p);
134
+ let files = [];
135
+ if (st.isDirectory()) {
136
+ walkFiles(p, p, files);
137
+ } else {
138
+ const ext = path.basename(p).includes('.') ? path.basename(p).split('.').pop().toLowerCase() : '';
139
+ if (ALLOWED_EXTS.has(ext)) files.push({ rel: path.basename(p), full: p });
140
+ else skipped.push(t);
141
+ }
142
+ for (const f of files) {
143
+ const dest = path.join(STAGE_FILES, f.rel);
144
+ fs.mkdirSync(path.dirname(dest), { recursive: true });
145
+ fs.copyFileSync(f.full, dest);
146
+ added.push(f.rel);
147
+ }
148
+ }
149
+
150
+ // 更新 manifest(去重合并)
151
+ let manifest = { files: [] };
152
+ try { manifest = JSON.parse(fs.readFileSync(STAGE_MANIFEST, 'utf8')); } catch (e) {}
153
+ const set = new Set(manifest.files || []);
154
+ added.forEach((f) => set.add(f));
155
+ manifest.files = Array.from(set).sort();
156
+ manifest.updated_at = new Date().toISOString();
157
+ fs.writeFileSync(STAGE_MANIFEST, JSON.stringify(manifest, null, 2));
158
+
159
+ if (added.length) {
160
+ ok('已加入暂存区 ' + added.length + ' 个文件:');
161
+ added.slice(0, 20).forEach((f) => console.log(' + ' + f));
162
+ if (added.length > 20) console.log(' … 等共 ' + added.length + ' 个');
163
+ }
164
+ if (skipped.length) warn('跳过不支持的文件:' + skipped.join(', '));
165
+ if (!added.length && !skipped.length) warn('没有可添加的文件');
166
+ else console.log('执行 op push 部署上线');
167
+ }
168
+
169
+ /* ---------- status ---------- */
170
+ function cmdStatus() {
171
+ if (!fs.existsSync(STAGE_DIR)) die('暂存区不存在,请先执行 op init');
172
+ const files = listStaged();
173
+ if (!files.length) { warn('暂存区为空,执行 op add 添加文件'); return; }
174
+ ok('暂存区共 ' + files.length + ' 个文件:');
175
+ let total = 0;
176
+ files.forEach((f) => {
177
+ total += f.size;
178
+ console.log(' ' + pad(f.rel, 40) + ' ' + (f.size / 1024).toFixed(1) + ' KB');
179
+ });
180
+ console.log('合计 ' + (total / 1024).toFixed(1) + ' KB');
181
+ }
182
+
183
+ function listStaged() {
184
+ const out = [];
185
+ (function walk(dir, base) {
186
+ for (const name of fs.readdirSync(dir)) {
187
+ const full = path.join(dir, name);
188
+ const rel = path.relative(base, full).split(path.sep).join('/');
189
+ const st = fs.statSync(full);
190
+ if (st.isDirectory()) walk(full, base);
191
+ else out.push({ rel, full, size: st.size });
192
+ }
193
+ })(STAGE_FILES, STAGE_FILES);
194
+ return out;
195
+ }
196
+
197
+ /* ---------- push ---------- */
198
+ async function cmdPush() {
199
+ const cfg = conf.load();
200
+ await requireAuth(cfg);
201
+ if (!fs.existsSync(STAGE_DIR)) die('暂存区不存在,请先执行 op init 和 op add');
202
+ const files = listStaged();
203
+ if (!files.length) die('暂存区为空,先执行 op add 添加文件');
204
+ if (!files.some((f) => f.rel.toLowerCase() === 'index.html')) {
205
+ die('缺少 index.html(网站入口文件),无法部署');
206
+ }
207
+
208
+ // 默认短链名:目录名
209
+ const dirName = path.basename(process.cwd()).replace(/[^A-Za-z0-9_\-]/g, '-').toLowerCase() || 'site';
210
+ const pathName = await ask('短链名(字母/数字/连字符/下划线)', dirName);
211
+ if (!/^[A-Za-z0-9_\-]{1,64}$/.test(pathName)) die('短链名只能包含字母、数字、连字符和下划线(≤64 字符)');
212
+
213
+ const previewAns = await ask('预览图路径(图片文件,输入 no 跳过)', 'no');
214
+ let previewPath = null;
215
+ if (previewAns && previewAns.toLowerCase() !== 'no') {
216
+ const pp = path.resolve(process.cwd(), previewAns);
217
+ if (!fs.existsSync(pp)) die('预览图不存在:' + previewAns);
218
+ const pext = path.basename(pp).split('.').pop().toLowerCase();
219
+ if (!['jpg', 'jpeg', 'png', 'gif', 'webp'].includes(pext)) die('预览图仅支持 jpg/png/gif/webp');
220
+ previewPath = pp;
221
+ }
222
+
223
+ const pubAns = await ask('是否公开(yes/no)', 'yes');
224
+ const isPublic = parseBool(pubAns, true);
225
+ const seoAns = await ask('是否 SEO 优化(yes/no)', 'yes');
226
+ const useSeo = parseBool(seoAns, true);
227
+
228
+ info('上传 ' + files.length + ' 个文件中…');
229
+ const form = new FormData();
230
+ form.append('path_name', pathName);
231
+ form.append('title', pathName);
232
+ form.append('description', '');
233
+ form.append('is_public', isPublic ? 'true' : 'false');
234
+ form.append('seo', useSeo ? 'true' : 'false');
235
+ for (const f of files) {
236
+ const buf = fs.readFileSync(f.full);
237
+ form.append('files', new Blob([buf]), f.rel);
238
+ }
239
+ if (previewPath) {
240
+ const buf = fs.readFileSync(previewPath);
241
+ form.append('preview', new Blob([buf]), path.basename(previewPath));
242
+ }
243
+
244
+ const r = await api(cfg, 'POST', '/api/cli/deploy', form, true);
245
+ if (r.status === 401) die('登录已失效,请重新执行 op login');
246
+ if (!r.data.ok) die(r.data.error || '部署失败');
247
+ ok('部署完成:' + r.data.url);
248
+ console.log('在浏览器打开:op open ' + pathName);
249
+ }
250
+
251
+ /* ---------- clone ---------- */
252
+ async function cmdClone(args) {
253
+ const cfg = conf.load();
254
+ await requireAuth(cfg);
255
+ const arg = args[0];
256
+ if (!arg) die('用法:op clone <站点URL或短链名> 例如 op clone https://app1.cloudstudio.site/my-site/');
257
+ let name = arg;
258
+ try {
259
+ const u = new URL(arg);
260
+ name = u.pathname.split('/').filter(Boolean)[0] || '';
261
+ } catch (e) { /* 不是 URL,按短链名处理 */ }
262
+ if (!/^[A-Za-z0-9_\-]{1,64}$/.test(name)) die('无法从参数解析出短链名:' + arg);
263
+
264
+ info('拉取 ' + name + ' …');
265
+ const r = await api(cfg, 'GET', '/api/cli/download/' + encodeURIComponent(name));
266
+ if (r.status === 401) die('登录已失效,请重新执行 op login');
267
+ if (!r.data.ok) die(r.data.error || '拉取失败');
268
+
269
+ const destDir = path.resolve(process.cwd(), name);
270
+ if (fs.existsSync(destDir)) die('目录已存在:' + name);
271
+ fs.mkdirSync(destDir, { recursive: true });
272
+ for (const f of r.data.files) {
273
+ const dest = path.join(destDir, ...f.path.split('/'));
274
+ fs.mkdirSync(path.dirname(dest), { recursive: true });
275
+ fs.writeFileSync(dest, Buffer.from(f.content, 'base64'));
276
+ }
277
+ ok('已拉取 ' + r.data.files.length + ' 个文件到 ./' + name + '/');
278
+ }
279
+
280
+ /* ---------- list ---------- */
281
+ async function cmdList() {
282
+ const cfg = conf.load();
283
+ const r = await requireAuth(cfg);
284
+ const sites = r.data.sites || [];
285
+ if (!sites.length) { warn('账号下还没有站点,执行 op init + op add + op push 创建'); return; }
286
+ ok('共 ' + sites.length + ' 个站点:');
287
+ table(
288
+ ['短链名', '标题', '公开', '浏览', '点赞', '更新时间', 'URL'],
289
+ sites.map((s) => [s.path_name, s.title, s.is_public ? '是' : '否', s.views, s.likes, s.updated_at, s.url])
290
+ );
291
+ }
292
+
293
+ /* ---------- delete ---------- */
294
+ async function cmdDelete(args) {
295
+ const cfg = conf.load();
296
+ await requireAuth(cfg);
297
+ const name = args[0];
298
+ if (!name) die('用法:op delete <短链名>');
299
+ const ans = await ask('确认删除站点 ' + name + '(yes/no)', 'no');
300
+ if (!parseBool(ans, false)) { info('已取消'); return; }
301
+ const r = await api(cfg, 'DELETE', '/api/cli/site/' + encodeURIComponent(name));
302
+ if (r.status === 401) die('登录已失效,请重新执行 op login');
303
+ if (!r.data.ok) die(r.data.error || '删除失败');
304
+ ok('已删除站点:' + name);
305
+ }
306
+
307
+ /* ---------- open ---------- */
308
+ async function cmdOpen(args) {
309
+ const cfg = conf.load();
310
+ await requireAuth(cfg);
311
+ const name = args[0];
312
+ if (!name) die('用法:op open <短链名>');
313
+ const url = cfg.server.replace(/\/$/, '') + '/' + name + '/';
314
+ const cmdLine = process.platform === 'darwin' ? 'open' :
315
+ process.platform === 'win32' ? 'start ""' : 'xdg-open';
316
+ exec(cmdLine + ' "' + url + '"', () => {});
317
+ ok('已在浏览器打开:' + url);
318
+ }
319
+
320
+ /* ---------- help ---------- */
321
+ function cmdHelp() {
322
+ console.log('OnePage CLI —— 在终端部署你的静态网页');
323
+ console.log('');
324
+ console.log('用法:op <命令> [参数]');
325
+ console.log('');
326
+ console.log('账号:');
327
+ console.log(' op login 登录(新邮箱自动注册,用户名默认邮箱 @ 前缀)');
328
+ console.log(' op whoami 查看当前登录账号');
329
+ console.log(' op logout 退出登录');
330
+ console.log('');
331
+ console.log('部署流程:');
332
+ console.log(' op init 在当前目录创建代码暂存区(.onepage/)');
333
+ console.log(' op add [路径] 把文件/目录加入暂存区(默认当前目录)');
334
+ console.log(' op status 查看暂存区文件');
335
+ console.log(' op push 部署暂存区代码上线(交互式输入短链名/预览图/公开/SEO)');
336
+ console.log('');
337
+ console.log('站点管理:');
338
+ console.log(' op list 列出本账号所有站点');
339
+ console.log(' op clone <url> 拉取站点代码到本地(支持 URL 或短链名)');
340
+ console.log(' op delete <name> 删除指定站点');
341
+ console.log(' op open <name> 在浏览器打开站点');
342
+ console.log('');
343
+ console.log('示例:');
344
+ console.log(' op login && op init && op add . && op push');
345
+ console.log(' op clone https://app1.cloudstudio.site/demo/');
346
+ }
347
+
348
+ /* ---------- main ---------- */
349
+ async function main() {
350
+ const argv = process.argv.slice(2);
351
+ const cmd = (argv[0] || 'help').toLowerCase();
352
+ const args = argv.slice(1);
353
+ try {
354
+ switch (cmd) {
355
+ case 'login': return await cmdLogin();
356
+ case 'whoami': return cmdWhoami();
357
+ case 'logout': return cmdLogout();
358
+ case 'init': return cmdInit();
359
+ case 'add': return cmdAdd(args);
360
+ case 'status': return cmdStatus();
361
+ case 'push': return await cmdPush();
362
+ case 'clone': return await cmdClone(args);
363
+ case 'list': case 'ls': return await cmdList();
364
+ case 'delete': case 'rm': return await cmdDelete(args);
365
+ case 'open': return await cmdOpen(args);
366
+ case 'help': case '--help': case '-h': return cmdHelp();
367
+ default:
368
+ err('未知命令:' + cmd);
369
+ cmdHelp();
370
+ process.exit(1);
371
+ }
372
+ } catch (e) {
373
+ die(e.message || String(e));
374
+ }
375
+ }
376
+
377
+ main();
package/lib/api.js ADDED
@@ -0,0 +1,22 @@
1
+ 'use strict';
2
+ // HTTP 封装(Node 18+ 原生 fetch,零依赖)
3
+ async function api(cfg, method, pathname, body, isForm) {
4
+ const headers = {};
5
+ if (cfg.token) headers['Authorization'] = 'Bearer ' + cfg.token;
6
+ if (!isForm) headers['Content-Type'] = 'application/json';
7
+ let res;
8
+ try {
9
+ res = await fetch(cfg.server + pathname, {
10
+ method: method,
11
+ headers: headers,
12
+ body: isForm ? body : (body !== undefined ? JSON.stringify(body) : undefined),
13
+ });
14
+ } catch (e) {
15
+ throw new Error('无法连接服务器 ' + cfg.server + '(' + e.message + ')');
16
+ }
17
+ let data = {};
18
+ try { data = await res.json(); } catch (e) { /* 忽略非 JSON 响应 */ }
19
+ return { status: res.status, data: data };
20
+ }
21
+
22
+ module.exports = { api };
package/lib/config.js ADDED
@@ -0,0 +1,22 @@
1
+ 'use strict';
2
+ // 配置读写:~/.onepage-cli.json
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const os = require('os');
6
+
7
+ const CONF_PATH = path.join(os.homedir(), '.onepage-cli.json');
8
+ const DEFAULTS = { server: 'https://app1.cloudstudio.site', token: '', username: '', email: '' };
9
+
10
+ function load() {
11
+ try {
12
+ return Object.assign({}, DEFAULTS, JSON.parse(fs.readFileSync(CONF_PATH, 'utf8')));
13
+ } catch (e) {
14
+ return Object.assign({}, DEFAULTS);
15
+ }
16
+ }
17
+
18
+ function save(cfg) {
19
+ fs.writeFileSync(CONF_PATH, JSON.stringify(cfg, null, 2), { mode: 0o600 });
20
+ }
21
+
22
+ module.exports = { load, save, CONF_PATH };
package/lib/prompt.js ADDED
@@ -0,0 +1,79 @@
1
+ 'use strict';
2
+ // 交互输入(git 风格提示)
3
+ // 统一处理两种模式:
4
+ // - TTY 交互:逐条提问,密码回显 *
5
+ // - 管道/脚本:预先读入所有行排队消费(避免多个 readline 实例丢行)
6
+ const readline = require('readline');
7
+
8
+ const queue = [];
9
+ let eof = false;
10
+ let interactive = !!process.stdin.isTTY;
11
+
12
+ if (!interactive) {
13
+ const rl = readline.createInterface({ input: process.stdin, terminal: false });
14
+ rl.on('line', (l) => queue.push(l));
15
+ rl.on('close', () => { eof = true; });
16
+ }
17
+
18
+ function nextLine() {
19
+ return new Promise((resolve) => {
20
+ if (queue.length) return resolve(queue.shift());
21
+ if (eof) return resolve('');
22
+ const t = setInterval(() => {
23
+ if (queue.length) { clearInterval(t); resolve(queue.shift()); }
24
+ else if (eof) { clearInterval(t); resolve(''); }
25
+ }, 20);
26
+ });
27
+ }
28
+
29
+ async function ask(question, def) {
30
+ const suffix = def ? ' [' + def + ']' : '';
31
+ if (interactive) {
32
+ return new Promise((resolve) => {
33
+ const r = readline.createInterface({ input: process.stdin, output: process.stdout });
34
+ r.question(question + suffix + ': ', (ans) => {
35
+ r.close();
36
+ const v = (ans || '').trim();
37
+ resolve(v || (def || ''));
38
+ });
39
+ });
40
+ }
41
+ const line = await nextLine();
42
+ const v = (line || '').trim();
43
+ return v || (def || '');
44
+ }
45
+
46
+ // 密码输入:TTY 下回显 *;非 TTY 直接读行
47
+ async function askHidden(question) {
48
+ if (!interactive) {
49
+ const line = await nextLine();
50
+ return (line || '').trim();
51
+ }
52
+ return new Promise((resolve) => {
53
+ const r = readline.createInterface({
54
+ input: process.stdin,
55
+ output: process.stdout,
56
+ terminal: true,
57
+ });
58
+ r.question(question + ': ', (answer) => {
59
+ r.close();
60
+ process.stdout.write('\n');
61
+ resolve((answer || '').trim());
62
+ });
63
+ // 拦截输出:问题文本照常打印,输入字符替换为 *
64
+ r._writeToOutput = function (stringToWrite) {
65
+ if (stringToWrite.includes(question)) {
66
+ r.output.write(stringToWrite);
67
+ } else {
68
+ r.output.write('*');
69
+ }
70
+ };
71
+ });
72
+ }
73
+
74
+ function parseBool(v, def) {
75
+ if (v === undefined || v === '') return def;
76
+ return /^(y|yes|true|1)$/i.test(String(v).trim());
77
+ }
78
+
79
+ module.exports = { ask, askHidden, parseBool };
package/lib/util.js ADDED
@@ -0,0 +1,41 @@
1
+ 'use strict';
2
+ // 终端输出辅助(ANSI 颜色 + 简易表格)
3
+ const C = {
4
+ reset: '\x1b[0m',
5
+ bold: '\x1b[1m',
6
+ dim: '\x1b[2m',
7
+ green: '\x1b[32m',
8
+ red: '\x1b[31m',
9
+ yellow: '\x1b[33m',
10
+ cyan: '\x1b[36m',
11
+ gray: '\x1b[90m',
12
+ };
13
+
14
+ function ok(msg) { console.log(C.green + '✓ ' + C.reset + msg); }
15
+ function err(msg) { console.error(C.red + '✗ ' + msg + C.reset); }
16
+ function info(msg) { console.log(C.cyan + '› ' + C.reset + msg); }
17
+ function warn(msg) { console.log(C.yellow + '! ' + msg + C.reset); }
18
+
19
+ function pad(s, n) {
20
+ s = String(s == null ? '' : s);
21
+ let w = 0;
22
+ for (const ch of s) w += ch.charCodeAt(0) > 255 ? 2 : 1; // 中文按 2 列宽
23
+ return w >= n ? s : s + ' '.repeat(n - w);
24
+ }
25
+
26
+ function table(headers, rows) {
27
+ const widths = headers.map((h) => displayWidth(h));
28
+ rows.forEach((r) => r.forEach((c, i) => { widths[i] = Math.max(widths[i], displayWidth(c)); }));
29
+ const line = (cells, sep) => cells.map((c, i) => pad(c, widths[i])).join(sep);
30
+ console.log(line(headers, ' '));
31
+ console.log(C.gray + widths.map((w) => '─'.repeat(w)).join(' ') + C.reset);
32
+ rows.forEach((r) => console.log(line(r, ' ')));
33
+ }
34
+
35
+ function displayWidth(s) {
36
+ let w = 0;
37
+ for (const ch of String(s == null ? '' : s)) w += ch.charCodeAt(0) > 255 ? 2 : 1;
38
+ return w;
39
+ }
40
+
41
+ module.exports = { ok, err, info, warn, table, pad };
package/package.json CHANGED
@@ -1,6 +1,30 @@
1
1
  {
2
2
  "name": "onepage-cli",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "1.0.0",
4
+ "description": "OnePage 命令行工具:在终端生成、部署、管理你的静态网页(git 风格指令 op login / init / add / push / clone / list / delete)",
5
+ "bin": {
6
+ "op": "bin/op.js"
7
+ },
8
+ "main": "bin/op.js",
9
+ "files": [
10
+ "bin/",
11
+ "lib/",
12
+ "README.md"
13
+ ],
14
+ "keywords": [
15
+ "onepage",
16
+ "cli",
17
+ "deploy",
18
+ "static-site",
19
+ "web",
20
+ "ai"
21
+ ],
22
+ "engines": {
23
+ "node": ">=18.0.0"
24
+ },
25
+ "license": "MIT",
26
+ "repository": {
27
+ "type": "url",
28
+ "url": "https://app1.cloudstudio.site"
29
+ }
30
+ }