u1s1-cli 0.20.2 → 1.1.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.
@@ -7,7 +7,7 @@ export declare function ensureDefaultSettings(): void;
7
7
  /** ≤0.4.0 wrote u1s1-dark/u1s1-light into the pi themes dir; remove them. */
8
8
  export declare function cleanupBrandThemes(): void;
9
9
  /**
10
- * u1s1 provider for SDK-based frontends (`u1s1 web`): pi composes
10
+ * u1s1 provider for SDK-based frontends (Desktop App): pi composes
11
11
  * <agentDir>/models.json above registered providers, so this exposes the same
12
12
  * models as the TUI's in-process extension without patching the frontend.
13
13
  * apiKey stays an env reference — the launcher sets U1S1_API_KEY, the file
@@ -15,7 +15,7 @@ export declare function cleanupBrandThemes(): void;
15
15
  * propagate; other providers a user may have added are preserved.
16
16
  */
17
17
  /**
18
- * 联网工具经 <agentDir>/extensions 投影:pi 的资源加载器对 TUI 和 `u1s1 web`
18
+ * 联网工具经 <agentDir>/extensions 投影:pi 的资源加载器对 TUI 和 Desktop App
19
19
  * 都会扫描这个目录,两个前端从此共用同一份工具注册,不用各接一遍。
20
20
  * 文件每次启动重写(baseUrl/服务端开关变化随之生效);apiKey 走 U1S1_API_KEY
21
21
  * 环境变量,文件里不落密钥。U1S1_TOOLS_VIA_EXTENSION 守卫:旧版 CLI 仍在
@@ -141,7 +141,7 @@ export function cleanupBrandThemes() {
141
141
  }
142
142
  }
143
143
  /**
144
- * u1s1 provider for SDK-based frontends (`u1s1 web`): pi composes
144
+ * u1s1 provider for SDK-based frontends (Desktop App): pi composes
145
145
  * <agentDir>/models.json above registered providers, so this exposes the same
146
146
  * models as the TUI's in-process extension without patching the frontend.
147
147
  * apiKey stays an env reference — the launcher sets U1S1_API_KEY, the file
@@ -149,7 +149,7 @@ export function cleanupBrandThemes() {
149
149
  * propagate; other providers a user may have added are preserved.
150
150
  */
151
151
  /**
152
- * 联网工具经 <agentDir>/extensions 投影:pi 的资源加载器对 TUI 和 `u1s1 web`
152
+ * 联网工具经 <agentDir>/extensions 投影:pi 的资源加载器对 TUI 和 Desktop App
153
153
  * 都会扫描这个目录,两个前端从此共用同一份工具注册,不用各接一遍。
154
154
  * 文件每次启动重写(baseUrl/服务端开关变化随之生效);apiKey 走 U1S1_API_KEY
155
155
  * 环境变量,文件里不落密钥。U1S1_TOOLS_VIA_EXTENSION 守卫:旧版 CLI 仍在
package/dist/deploy.d.ts CHANGED
@@ -1,2 +1,10 @@
1
1
  import { type CliConfig } from "./config.js";
2
+ type SiteVisibility = "public" | "private";
3
+ interface DeployArgs {
4
+ name?: string;
5
+ dirArg?: string;
6
+ visibility?: SiteVisibility;
7
+ }
8
+ export declare function parseDeployArgs(args: string[]): DeployArgs;
2
9
  export declare function deployCommand(cfg: CliConfig, args: string[]): Promise<void>;
10
+ export {};
package/dist/deploy.js CHANGED
@@ -84,6 +84,37 @@ function fmtBytes(n) {
84
84
  return `${Math.round(n / 1024)}KB`;
85
85
  return `${n}B`;
86
86
  }
87
+ export function parseDeployArgs(args) {
88
+ const parsed = {};
89
+ for (let i = 0; i < args.length; i++) {
90
+ const arg = args[i];
91
+ if (arg === "--name" || arg === "-n") {
92
+ const value = args[++i];
93
+ if (!value || value.startsWith("-"))
94
+ throw new Error(`${arg} 后面需要站点名`);
95
+ parsed.name = value.toLowerCase();
96
+ }
97
+ else if (arg.startsWith("--name=")) {
98
+ parsed.name = arg.slice(7).toLowerCase();
99
+ if (!parsed.name)
100
+ throw new Error("--name 后面需要站点名");
101
+ }
102
+ else if (arg === "--public" || arg === "--private") {
103
+ const visibility = arg.slice(2);
104
+ if (parsed.visibility && parsed.visibility !== visibility) {
105
+ throw new Error("--public 和 --private 不能同时使用");
106
+ }
107
+ parsed.visibility = visibility;
108
+ }
109
+ else if (arg.startsWith("-")) {
110
+ throw new Error(`不认识的 deploy 参数:${arg}`);
111
+ }
112
+ else {
113
+ parsed.dirArg = arg;
114
+ }
115
+ }
116
+ return parsed;
117
+ }
87
118
  async function api(cfg, method, path, body) {
88
119
  let resp;
89
120
  try {
@@ -144,8 +175,26 @@ async function promptSiteName(def) {
144
175
  rl.close();
145
176
  }
146
177
  }
178
+ async function promptVisibility() {
179
+ if (!process.stdin.isTTY)
180
+ return "private";
181
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
182
+ try {
183
+ while (true) {
184
+ const answer = (await rl.question(" 发布方式 [1] 公开(进入作品社区) [2] 私密(仅自己登录后可看,默认):")).trim().toLowerCase();
185
+ if (!answer || answer === "2" || answer === "private" || answer === "私密")
186
+ return "private";
187
+ if (answer === "1" || answer === "public" || answer === "公开")
188
+ return "public";
189
+ console.log(" 请输入 1 或 2");
190
+ }
191
+ }
192
+ finally {
193
+ rl.close();
194
+ }
195
+ }
147
196
  export async function deployCommand(cfg, args) {
148
- // 参数:[dir] [--name xxx];u1s1 deploy list 列出已有站点
197
+ // 参数:[dir] [--name xxx] [--public|--private];u1s1 deploy list 列出已有站点
149
198
  if (args[0] === "list") {
150
199
  const { sites } = await api(cfg, "GET", "/deploy/sites");
151
200
  if (!sites.length) {
@@ -154,22 +203,14 @@ export async function deployCommand(cfg, args) {
154
203
  }
155
204
  console.log("");
156
205
  for (const s of sites) {
157
- console.log(` ${s.deployed ? "" : ""} ${s.url} ${fmtBytes(s.total_bytes)} · ${s.updated_at} UTC`);
206
+ const visibility = s.visibility === "private" ? "私密" : s.community_listed ? "公开(社区)" : "公开(未展示)";
207
+ console.log(` ${s.deployed ? "●" : "○"} ${s.url} ${visibility} · ${fmtBytes(s.total_bytes)} · ${s.updated_at} UTC`);
158
208
  }
159
209
  console.log("");
160
210
  return;
161
211
  }
162
- let name;
163
- let dirArg;
164
- for (let i = 0; i < args.length; i++) {
165
- const a = args[i];
166
- if (a === "--name" || a === "-n")
167
- name = args[++i]?.toLowerCase();
168
- else if (a.startsWith("--name="))
169
- name = a.slice(7).toLowerCase();
170
- else if (!a.startsWith("-"))
171
- dirArg = a;
172
- }
212
+ const parsed = parseDeployArgs(args);
213
+ let { name, dirArg, visibility } = parsed;
173
214
  const dir = resolveSiteDir(dirArg);
174
215
  const files = collectFiles(dir);
175
216
  if (!files.some((f) => f.path === "index.html")) {
@@ -187,6 +228,10 @@ export async function deployCommand(cfg, args) {
187
228
  const projectName = BUILD_DIRS.includes(basename(dir)) ? basename(resolve(dir, "..")) : basename(dir);
188
229
  name = await promptSiteName(slugify(projectName));
189
230
  }
231
+ // 同一目录再次部署时保持上次设置;首次部署显式选择。CI/脚本没有交互时
232
+ // 取更安全的 private,用户仍可用 --public 明确公开。
233
+ if (!visibility && remembered !== name)
234
+ visibility = await promptVisibility();
190
235
  let start;
191
236
  for (let attempt = 0; !start; attempt++) {
192
237
  try {
@@ -222,12 +267,22 @@ export async function deployCommand(cfg, args) {
222
267
  const fin = await api(cfg, "POST", "/deploy/finish", {
223
268
  site: start.site,
224
269
  deploy_id: start.deploy_id,
270
+ visibility,
225
271
  });
226
272
  rememberSite(dir, start.site);
227
273
  console.log(` ✅ 部署完成,${fin.file_count} 个文件已上线`);
228
274
  console.log("");
229
- console.log(` 🌐 ${fin.url}`);
275
+ console.log(` ${fin.visibility === "private" ? "🔒" : "🌐"} ${fin.url}`);
230
276
  console.log("");
231
- console.log(" 把网址发给朋友就能看。改完代码再跑一次 u1s1 deploy 即可更新。");
277
+ if (fin.visibility === "private") {
278
+ console.log(" 私密站点仅你可见,请从 https://u1s1.io/dashboard#sec-sites 打开。");
279
+ }
280
+ else if (fin.community_listed) {
281
+ console.log(" 已进入 https://u1s1.io/community ,把网址发给朋友就能看。");
282
+ }
283
+ else {
284
+ console.log(" 公开链接可直接访问;运行 u1s1 deploy --public 可明确加入作品社区。");
285
+ }
286
+ console.log(" 改完代码再跑一次 u1s1 deploy 即可更新。");
232
287
  console.log("");
233
288
  }
package/dist/index.js CHANGED
@@ -171,7 +171,7 @@ async function runAgent(cfg, args) {
171
171
  ensureBrandPrompt(await shellReady);
172
172
  ensureProviderModels(officialCfg);
173
173
  ensureWorkflowPromptTemplate();
174
- // 联网工具经 agentDir/extensions 投影,TUI 和 u1s1 web 共用一份注册
174
+ // 联网工具经 agentDir/extensions 投影,TUI 和 Desktop App 共用一份注册
175
175
  writeWebToolsExtension(officialCfg, {
176
176
  webSearch: webSearchEnabled,
177
177
  webFetchRender: webFetchRenderEnabled,
@@ -314,20 +314,12 @@ async function run() {
314
314
  }
315
315
  if (cmd === "--help" || cmd === "-h") {
316
316
  printConsoleBanner(VERSION);
317
- console.log(" u1s1 命令:web(浏览器网页版)· web shortcut(桌面图标)· deploy(发布网页)· login / logout · model · usage · update · import · bench");
317
+ console.log(" u1s1 命令:deploy(发布网页,可选 --public / --private)· login / logout · model · usage · update · import · bench");
318
318
  console.log("");
319
319
  }
320
320
  if (cmd === "web") {
321
- // shortcut 不需要登录:安装脚本装完立刻调用,不能在这里卡住 device login
322
- if (args[1] === "shortcut") {
323
- const { createWebShortcut } = await import("./shortcut.js");
324
- createWebShortcut();
325
- return;
326
- }
327
- const { ensureAuth } = await import("./login.js");
328
- const cfg = await ensureAuth();
329
- const { webCommand } = await import("./web.js");
330
- await webCommand(cfg, args.slice(1));
321
+ console.error("u1s1 web 已下线,请使用 Desktop App:https://u1s1.io/#download");
322
+ process.exitCode = 1;
331
323
  return;
332
324
  }
333
325
  if (cmd === "deploy") {
package/dist/web.d.ts CHANGED
@@ -1,13 +1,7 @@
1
1
  import { type CliConfig } from "./config.js";
2
2
  /**
3
- * `u1s1 web` 浏览器网页版。薄包装 pi-web-ui 的服务器:注入我们的 agentDir
4
- * (品牌 prompt / models.json / 会话与 TUI 共享)和登录 key,其余原样透传
5
- * (--port / --cwd / --no-browser)。
6
- */
7
- /**
8
- * 启动 pi-web-ui 服务器前的公共准备:品牌 prompt、模型列表、auth.json 凭据、
9
- * 联网工具扩展、默认模型,并返回子进程需要的环境变量。CLI(`u1s1 web`)和
10
- * 桌面版 App(packages/app,经 "u1s1-cli/embed" 引入)共用这一份逻辑。
3
+ * Desktop App 启动 agent server 前的公共准备:品牌 prompt、模型列表、
4
+ * auth.json 凭据、联网工具扩展、默认模型,并返回子进程需要的环境变量。
5
+ * App "u1s1-cli/embed" 引入;CLI 本身不提供浏览器版入口。
11
6
  */
12
7
  export declare function prepareWebEnv(cfg: CliConfig): Promise<Record<string, string>>;
13
- export declare function webCommand(cfg: CliConfig, args: string[]): Promise<void>;
package/dist/web.js CHANGED
@@ -1,31 +1,22 @@
1
- import { spawn, spawnSync } from "node:child_process";
2
1
  import { mkdirSync } from "node:fs";
3
- import { createRequire } from "node:module";
4
- import { dirname, join } from "node:path";
2
+ import { join } from "node:path";
5
3
  import { cleanupBrandThemes, endpointKeyEnv, ensureAuthCredential, ensureBrandPrompt, ensureWorkflowPromptTemplate, ensureDefaultSettings, ensureProviderModels, scrubForeignProviderEnv, writeAttributionExtension, writeWebToolsExtension, } from "./agent-setup.js";
6
4
  import { agentDir, apiModelToDef, resolvePreferredModel, setModelsFromApi, u1s1Dir, writeAgentDefaultModel, } from "./config.js";
7
5
  import { ensureSearchTools } from "./search-tools.js";
8
6
  import { ensureUsableShell } from "./shell-doctor.js";
9
- import { applyWebUiBranding, applyWebUiFrontend } from "./webui-brand.js";
10
7
  import { fetchModels, loadCustomEndpoints } from "./api.js";
11
8
  import { ensureSigningProxy } from "./device-auth.js";
12
- const require = createRequire(import.meta.url);
13
9
  /**
14
- * `u1s1 web` 浏览器网页版。薄包装 pi-web-ui 的服务器:注入我们的 agentDir
15
- * (品牌 prompt / models.json / 会话与 TUI 共享)和登录 key,其余原样透传
16
- * (--port / --cwd / --no-browser)。
17
- */
18
- /**
19
- * 启动 pi-web-ui 服务器前的公共准备:品牌 prompt、模型列表、auth.json 凭据、
20
- * 联网工具扩展、默认模型,并返回子进程需要的环境变量。CLI(`u1s1 web`)和
21
- * 桌面版 App(packages/app,经 "u1s1-cli/embed" 引入)共用这一份逻辑。
10
+ * Desktop App 启动 agent server 前的公共准备:品牌 prompt、模型列表、
11
+ * auth.json 凭据、联网工具扩展、默认模型,并返回子进程需要的环境变量。
12
+ * App "u1s1-cli/embed" 引入;CLI 本身不提供浏览器版入口。
22
13
  */
23
14
  export async function prepareWebEnv(cfg) {
24
15
  cleanupBrandThemes();
25
16
  ensureDefaultSettings();
26
17
  // Windows shell 体检异步先行,与取模型列表并行,写 brand prompt 前 await
27
18
  const shellReady = ensureUsableShell();
28
- // 网页版/App 的 agent 会话同样依赖 fd/rg(同一个 agentDir);与取模型列表并行
19
+ // App 的 agent 会话同样依赖 fd/rg(同一个 agentDir);与取模型列表并行
29
20
  const searchToolsReady = ensureSearchTools(cfg);
30
21
  // Fetch model list from server; fall back to built-in MODELS on error.
31
22
  // 服务端没开搜索(或老网关没有 features 字段)时不注册 web_search
@@ -47,10 +38,10 @@ export async function prepareWebEnv(cfg) {
47
38
  console.error(" 获取模型列表失败,使用内置列表:", e.message);
48
39
  }
49
40
  await endpointsReady;
50
- const signing = await ensureSigningProxy(cfg, "web");
41
+ const signing = await ensureSigningProxy(cfg, "desktop");
51
42
  const officialCfg = { ...cfg, baseUrl: signing.baseUrl, apiKey: signing.localKey };
52
43
  ensureBrandPrompt(await shellReady);
53
- // /workflow 提示词模板与 TUI 同源,web/桌面版也要有
44
+ // /workflow 提示词模板与 TUI 同源,Desktop App 也要有
54
45
  ensureWorkflowPromptTemplate();
55
46
  ensureProviderModels(officialCfg);
56
47
  // pi-web-ui 靠 auth.json 判断「已配置」,否则网页会弹 pi 安装引导
@@ -63,13 +54,13 @@ export async function prepareWebEnv(cfg) {
63
54
  });
64
55
  // OpenRouter 应用归因:直连流量计入 u1s1 的公开排行(与 TUI 同源)
65
56
  writeAttributionExtension();
66
- // 网页版新会话从 settings.json 的 defaultModel 取模型(TUI 是每次传 --model),
57
+ // Desktop App 新会话从 settings.json 的 defaultModel 取模型(TUI 是每次传 --model),
67
58
  // 确保它有值;resolvePreferredModel 优先尊重已有的 in-session 选择,不会回退覆盖。
68
59
  const pref = resolvePreferredModel(cfg);
69
60
  writeAgentDefaultModel(pref.provider, pref.id);
70
61
  const dataDir = join(u1s1Dir, "web");
71
62
  mkdirSync(dataDir, { recursive: true });
72
- // web 子进程按 {...process.env, ...返回值} 起,这里剔掉即对子进程生效
63
+ // App 子进程按 {...process.env, ...返回值} 起,这里剔掉即对子进程生效
73
64
  scrubForeignProviderEnv();
74
65
  await searchToolsReady;
75
66
  return {
@@ -77,62 +68,7 @@ export async function prepareWebEnv(cfg) {
77
68
  U1S1_API_KEY: officialCfg.apiKey,
78
69
  U1S1_TOOLS_VIA_EXTENSION: "1",
79
70
  PI_WEB_DATA_DIR: dataDir,
80
- // 自定义端点的密钥经环境变量传给 web 子进程(models.json 里只有 $VAR 引用)
71
+ // 自定义端点的密钥经环境变量传给 App 子进程(models.json 里只有 $VAR 引用)
81
72
  ...endpointKeyEnv(),
82
73
  };
83
74
  }
84
- export async function webCommand(cfg, args) {
85
- // pi-web-ui 的 server install/shortcut 生成的服务定义只固化 PORT/CWD,不带
86
- // PI_CODING_AGENT_DIR 和 U1S1_API_KEY —— 装出来的自启服务是坏的(丢登录态、
87
- // 会话落到 ~/.pi)。在上游支持透传 env 或我们自己生成服务定义前,直接拦掉。
88
- if (args[0] === "server") {
89
- console.error("u1s1 web 暂不支持 server 子命令(开机自启/桌面图标),后续版本会加上。");
90
- console.error("现在请直接运行 u1s1 web,Ctrl+C 停止。");
91
- process.exitCode = 1;
92
- return;
93
- }
94
- // pi-web-ui 是 optionalDependency:装 CLI 时它编译/下载失败不会拖垮整个安装,
95
- // 代价是这里可能不存在 —— 给出人话指引而不是 Cannot find module。
96
- // (桌面版 App 上线后这里的兜底会改为指向 App 下载,CLI 届时不再携带 pi-web-ui。)
97
- let bin;
98
- try {
99
- bin = require.resolve("pi-web-ui/bin/pi-web-ui.mjs");
100
- }
101
- catch {
102
- console.error("网页版组件(pi-web-ui)没有安装成功,通常是安装时网络不稳或缺少编译工具。");
103
- console.error("请重新安装试试:npm install -g u1s1-cli");
104
- console.error("Linux 用户可能还需要:sudo apt install -y build-essential(或对应发行版的 gcc/make)");
105
- process.exitCode = 1;
106
- return;
107
- }
108
- // 终端面板依赖原生模块 node-pty。Windows/macOS 走官方 prebuilds 一定在;
109
- // Linux 便携包/无 gcc 环境可能缺编译产物,而服务器是顶层 import —— 缺了会
110
- // 整个起不来。先探测,坏了就说人话。
111
- const probe = spawnSync(process.execPath, ["-e", "require(require.resolve('node-pty',{paths:[process.argv[1]]}))", dirname(bin)], { timeout: 10_000 });
112
- if (probe.status !== 0) {
113
- console.error("网页版的终端组件(node-pty)在这台机器上没有编译成功,网页版暂时起不来。");
114
- console.error("Linux 用户先装编译工具再重装:sudo apt install -y build-essential && npm install -g u1s1-cli");
115
- process.exitCode = 1;
116
- return;
117
- }
118
- const webEnv = await prepareWebEnv(cfg);
119
- // 首选 u1s1 自有前端(Codex 布局);产物缺失时回退为上游界面 + 品牌补丁
120
- if (!applyWebUiFrontend(bin))
121
- applyWebUiBranding(bin);
122
- console.log("u1s1 网页版启动中… 浏览器会自动打开;关闭请回到这里按 Ctrl+C。");
123
- const child = spawn(process.execPath, [bin, ...args], {
124
- stdio: "inherit",
125
- env: { ...process.env, ...webEnv },
126
- });
127
- // Ctrl+C 由子进程负责优雅停机(同进程组,信号会直达);父进程只等退出码,
128
- // 避免父进程先死导致孤儿服务器。
129
- const ignore = () => { };
130
- process.on("SIGINT", ignore);
131
- process.on("SIGTERM", ignore);
132
- await new Promise((resolve) => {
133
- child.on("exit", (code) => {
134
- process.exitCode = code ?? 0;
135
- resolve();
136
- });
137
- });
138
- }
@@ -29,8 +29,8 @@ export function applyWebUiFrontend(binPath) {
29
29
  }
30
30
  }
31
31
  /**
32
- * pi-web-ui 前端产物的启动时品牌补丁。不 fork 上游,只在每次 `u1s1 web` /
33
- * 桌面版启动前改它 web/dist 里的静态文件:重写 <title>、换 favicon、注入
32
+ * pi-web-ui 前端产物的启动时品牌补丁。不 fork 上游,只在 Desktop App
33
+ * 启动前改它 web/dist 里的静态文件:重写 <title>、换 favicon、注入
34
34
  * 品牌脚本和一份 Codex 风格的主题 CSS(u1s1-theme.css),把整个界面换成
35
35
  * u1s1 的视觉:石墨底色、品牌蓝 #4ea1ff、幽灵化顶栏、居中内容列。
36
36
  *
@@ -40,7 +40,7 @@ export function applyWebUiFrontend(binPath) {
40
40
  * 原地写会污染 store;unlink 后新写的是独立 inode,npm 平铺安装则无所谓。
41
41
  */
42
42
  const PATCH_MARK = "u1s1-brand-v2";
43
- const PAGE_TITLE = "u1s1 网页版 — 有一说一";
43
+ const PAGE_TITLE = "u1s1 Desktop App — 有一说一";
44
44
  const FAVICON_SVG = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
45
45
  <rect width="64" height="64" rx="14" fill="#101418"/>
46
46
  <text x="32" y="42" font-family="ui-monospace,Menlo,Consolas,monospace" font-size="26" font-weight="700" fill="#4ea1ff" text-anchor="middle">u1</text>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "u1s1-cli",
3
- "version": "0.20.2",
3
+ "version": "1.1.0",
4
4
  "description": "u1s1 — 有一说一,最省心的 AI 编程搭子。终端里用中文说需求,AI 帮你读文件、改代码、跑命令。",
5
5
  "type": "module",
6
6
  "bin": {
@@ -15,7 +15,6 @@
15
15
  },
16
16
  "files": [
17
17
  "dist",
18
- "webui-dist",
19
18
  "bench",
20
19
  "scripts"
21
20
  ],
@@ -47,9 +46,6 @@
47
46
  "@earendil-works/pi-tui": "0.84.3",
48
47
  "typebox": "1.3.7"
49
48
  },
50
- "optionalDependencies": {
51
- "pi-web-ui": "0.26.0"
52
- },
53
49
  "devDependencies": {
54
50
  "@types/node": "^22.20.1",
55
51
  "tsx": "^4.23.12",
@@ -1 +0,0 @@
1
- export declare function createWebShortcut(): void;
package/dist/shortcut.js DELETED
@@ -1,106 +0,0 @@
1
- import { spawnSync } from "node:child_process";
2
- import { chmodSync, existsSync, mkdirSync, writeFileSync } from "node:fs";
3
- import { homedir } from "node:os";
4
- import { join } from "node:path";
5
- import { fileURLToPath } from "node:url";
6
- /**
7
- * `u1s1 web shortcut` — 在桌面创建「u1s1 网页版」图标,双击即启动网页版。
8
- * 面向完全不用终端的用户:安装脚本会自动调用,之后的登录(浏览器 device flow)、
9
- * 聊天全程不需要终端知识。
10
- *
11
- * 图标里嵌的是 node 与 CLI 入口的绝对路径 —— GUI 双击环境(macOS .command、
12
- * Linux .desktop、Windows .lnk)拿不到 npm 全局 bin 的 PATH。
13
- * 工作目录固定 ~/u1s1(自动创建):给新手一个可预期的「我的项目」目录,
14
- * 网页里仍可切换;有历史工作区时 UI 会自己恢复上次的。
15
- */
16
- const SHORTCUT_NAME = "u1s1 网页版";
17
- function cliEntry() {
18
- return fileURLToPath(new URL("./index.js", import.meta.url));
19
- }
20
- function defaultWorkspace() {
21
- const dir = join(homedir(), "u1s1");
22
- mkdirSync(dir, { recursive: true });
23
- return dir;
24
- }
25
- function linuxDesktopDir() {
26
- const out = spawnSync("xdg-user-dir", ["DESKTOP"], { encoding: "utf8", timeout: 3000 });
27
- const dir = out.status === 0 ? out.stdout.trim() : "";
28
- return dir && dir !== homedir() ? dir : join(homedir(), "Desktop");
29
- }
30
- function createLinux(node, entry, cwd) {
31
- const desktop = `[Desktop Entry]
32
- Type=Application
33
- Name=${SHORTCUT_NAME}
34
- Comment=有一说一 — 说人话的 AI 编程搭子(浏览器版)
35
- Exec="${node}" "${entry}" web --cwd "${cwd}"
36
- Terminal=true
37
- Categories=Development;
38
- `;
39
- const appsDir = join(homedir(), ".local", "share", "applications");
40
- mkdirSync(appsDir, { recursive: true });
41
- const appFile = join(appsDir, "u1s1-web.desktop");
42
- writeFileSync(appFile, desktop);
43
- chmodSync(appFile, 0o755);
44
- const desktopDir = linuxDesktopDir();
45
- let placed = appFile;
46
- if (existsSync(desktopDir)) {
47
- placed = join(desktopDir, "u1s1-web.desktop");
48
- writeFileSync(placed, desktop);
49
- chmodSync(placed, 0o755);
50
- // GNOME 桌面图标需要 trusted 标记才可双击;失败无妨(应用菜单入口仍可用)
51
- spawnSync("gio", ["set", placed, "metadata::trusted", "true"], { timeout: 3000 });
52
- }
53
- return placed;
54
- }
55
- function createMac(node, entry, cwd) {
56
- const file = join(homedir(), "Desktop", `${SHORTCUT_NAME}.command`);
57
- writeFileSync(file, `#!/bin/bash
58
- # 双击启动 u1s1 网页版;关闭这个窗口即停止
59
- exec "${node}" "${entry}" web --cwd "${cwd}"
60
- `);
61
- chmodSync(file, 0o755);
62
- return file;
63
- }
64
- function createWindows(node, entry, cwd) {
65
- // Desktop 路径交给 .NET 解析(兼容 OneDrive 重定向);.lnk 目标是 node.exe。
66
- const psQuote = (s) => `'${s.replace(/'/g, "''")}'`;
67
- const script = [
68
- `$desktop = [Environment]::GetFolderPath('Desktop')`,
69
- `$lnk = Join-Path $desktop ${psQuote(`${SHORTCUT_NAME}.lnk`)}`,
70
- `$ws = New-Object -ComObject WScript.Shell`,
71
- `$s = $ws.CreateShortcut($lnk)`,
72
- `$s.TargetPath = ${psQuote(node)}`,
73
- `$s.Arguments = ('"' + ${psQuote(entry)} + '" web --cwd "' + ${psQuote(cwd)} + '"')`,
74
- `$s.WorkingDirectory = ${psQuote(cwd)}`,
75
- `$s.Description = ${psQuote("有一说一 — 说人话的 AI 编程搭子(浏览器版)")}`,
76
- `$s.Save()`,
77
- `Write-Output $lnk`,
78
- ].join("; ");
79
- const out = spawnSync("powershell", ["-NoProfile", "-NonInteractive", "-Command", script], {
80
- encoding: "utf8",
81
- timeout: 20_000,
82
- });
83
- if (out.status !== 0) {
84
- throw new Error(`PowerShell 创建快捷方式失败:${(out.stderr || "").trim().slice(0, 200)}`);
85
- }
86
- return out.stdout.trim().split(/\r?\n/).pop() ?? "桌面";
87
- }
88
- export function createWebShortcut() {
89
- const node = process.execPath;
90
- const entry = cliEntry();
91
- const cwd = defaultWorkspace();
92
- try {
93
- const placed = process.platform === "darwin"
94
- ? createMac(node, entry, cwd)
95
- : process.platform === "win32"
96
- ? createWindows(node, entry, cwd)
97
- : createLinux(node, entry, cwd);
98
- console.log(`✓ 已在桌面创建「${SHORTCUT_NAME}」图标:${placed}`);
99
- console.log(" 双击即可在浏览器里使用 u1s1;项目文件默认放在 ~/u1s1 文件夹。");
100
- }
101
- catch (e) {
102
- console.error(`创建桌面图标失败:${e instanceof Error ? e.message : String(e)}`);
103
- console.error("可以先手动使用:打开终端输入 u1s1 web");
104
- process.exitCode = 1;
105
- }
106
- }