bookmark-research 0.5.0-beta.18 → 0.5.0-beta.20

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.
@@ -9,7 +9,7 @@
9
9
  "source": {
10
10
  "source": "npm",
11
11
  "package": "bookmark-research",
12
- "version": "0.5.0-beta.18",
12
+ "version": "0.5.0-beta.20",
13
13
  "registry": "https://registry.npmjs.org"
14
14
  },
15
15
  "policy": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bookmark-research",
3
- "version": "0.5.0-beta.18",
3
+ "version": "0.5.0-beta.20",
4
4
  "description": "Check bookmark URLs or Canvas packages, compare sources with host-led research, and retain verifiable reports.",
5
5
  "author": {
6
6
  "name": "kwenxu"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bookmark-research",
3
- "version": "0.5.0-beta.18",
3
+ "version": "0.5.0-beta.20",
4
4
  "description": "Check bookmark URLs or Canvas packages, compare sources with host-led research, and retain verifiable reports.",
5
5
  "license": "GPL-3.0-only",
6
6
  "repository": {
@@ -583,8 +583,16 @@ def _run_target(args, target, language):
583
583
 
584
584
 
585
585
  def _run_setup(args, installed, host, language, guided):
586
+ from settings import Settings
587
+ existing_config = Settings().path.is_file()
588
+ interactive = guided and (getattr(args, "interaction", "auto") == "always" or not existing_config)
589
+ if guided and existing_config and getattr(args, "interaction", "auto") != "always":
590
+ print(_message(language,
591
+ "Retained existing configuration (%s). Run `bookmark-research setup` to modify preferences or keys." % Settings().path,
592
+ "✓ 沿用已有研究偏好与密钥配置(%s);如需修改请运行 bookmark-research setup。" % Settings().path),
593
+ file=sys.stderr)
586
594
  command = [sys.executable, "-B", str(installed / "src/cli.py"), "setup", "--lang", language,
587
- "--host", _setup_host(host), "--interactive" if guided else "--non-interactive"]
595
+ "--host", _setup_host(host), "--interactive" if interactive else "--non-interactive"]
588
596
  if args.preferences:
589
597
  command.extend(["--input", args.preferences])
590
598
  if args.skip_checks:
@@ -592,7 +600,7 @@ def _run_setup(args, installed, host, language, guided):
592
600
  if args.test_retrieval:
593
601
  command.append("--test-retrieval")
594
602
  if not (installed / "src/onboarding.py").is_file():
595
- requested = guided or bool(args.preferences) or args.test_retrieval
603
+ requested = interactive or bool(args.preferences) or args.test_retrieval
596
604
  return {"completed": False if requested else None, "status": "unsupported",
597
605
  "message": "The installed version predates guided setup; install a newer version to use setup."}
598
606
  # The installed version owns its settings schema. Do not use the
package/src/onboarding.py CHANGED
@@ -108,7 +108,7 @@ class Console(Prompter):
108
108
  with reader, writer:
109
109
  yield cls(reader, writer, language)
110
110
 
111
- def install_targets(self, args):
111
+ def install_targets(self, args, existing_targets=None):
112
112
  self.intro("Bookmark Research — guided installation", "Bookmark Research — 引导安装")
113
113
  requested = args.host
114
114
  if isinstance(requested, str):
@@ -119,57 +119,171 @@ class Console(Prompter):
119
119
  client = resolve_dsh(getattr(args, "dsh", None))
120
120
  if client["source"] == "desktop":
121
121
  args.dsh = client["binary"]
122
- if requested:
123
- hosts = list(dict.fromkeys(requested))
124
- else:
125
- found = {name: bool(shutil.which(getattr(args, name, None) or name)) for name, _, _ in HOSTS}
126
- hints = {name: ("", None) if found[name] else ("CLI not found", "未找到 CLI") for name in found}
127
- found["dsh"], hints["dsh"] = client["usable"], _dsh_hint(client)
128
- if not any(found.values()):
129
- self.warn("No supported host CLI (codex, claude, dsh) was found on PATH.",
130
- "PATH 中未找到任何受支持的宿主 CLI(codex、claude、dsh)。")
131
- raise ValueError("No host CLI found; install Codex, Claude Code or DeepSeek Harness (dsh), "
132
- "or pass --codex/--claude/--dsh PATH to point at its executable")
133
- hosts = self.multiselect("Install into which hosts?", "安装到哪些宿主?", [
134
- Option(name, en, zh, *hints[name], disabled=not found[name]) for name, en, zh in HOSTS],
135
- [name for name, _, _ in HOSTS if found[name]])
136
- # A carrier that ships its own pnpm is the Desktop CLI, however it was found:
137
- # only that CLI may manage the profile name Desktop reserves for itself.
138
- desktop = client["bundled_pnpm"] is not None
139
- targets = []
140
- for host in hosts:
141
- label = next((en for name, en, _ in HOSTS if name == host), host)
142
- target = {"host": host, "scope": None, "project": None, "profile": None}
143
- if host == "claude":
144
- scope = args.scope
145
- if scope is None:
146
- options = [Option("user", "This user", "当前用户"), Option("project", "This project", "指定项目")]
147
- if host == "claude":
148
- options.append(Option("local", "This project, only for me", "指定项目(仅自己)"))
149
- scope = self.select(label + ": installation scope", label + ":安装范围", options, "user")
150
- target["scope"] = scope
151
- if scope in ("project", "local"):
152
- target["project"] = args.project or self.input(
153
- label + ": project directory", label + ":项目目录", str(Path.cwd()), _directory_problem)
154
- elif host == "dsh":
155
- # Never guess a DSH profile: it selects a distinct composed runtime.
156
- target["profile"] = args.profile or self.dsh_profile(desktop)
157
- targets.append(target)
158
- lines = []
159
- for target in targets:
160
- label = next((en for name, en, _ in HOSTS if name == target["host"]), target["host"])
161
- detail = ", ".join(key + "=" + target[key] for key in ("scope", "project", "profile") if target[key])
162
- lines.append((label + (": " + detail if detail else ""), label + (":" + detail if detail else "")))
163
- self.note("Installation plan", "安装计划", lines)
164
- if client["source"] == "desktop" and any(target["host"] == "dsh" for target in targets):
165
- self.note("DeepSeek Harness (dsh) CLI", "DeepSeek Harness (dsh) CLI", [(client["binary"], client["binary"])])
166
- if not self.confirm("Install now?", "现在安装?", True):
167
- raise KeyboardInterrupt
168
- first = targets[0]
169
- args.host, args.scope, args.project, args.profile = first["host"], first["scope"], first["project"], first["profile"]
170
- return targets
171
-
172
- def dsh_profile(self, desktop):
122
+ if existing_targets is None and not requested:
123
+ try:
124
+ scripts_dir = Path(__file__).resolve().parents[1] / "scripts"
125
+ if str(scripts_dir) not in sys.path:
126
+ sys.path.insert(0, str(scripts_dir))
127
+ from launcher import installed_targets
128
+ existing_targets = installed_targets(codex=getattr(args, "codex", "codex"))
129
+ except Exception:
130
+ existing_targets = []
131
+ if existing_targets and not requested:
132
+ target_version = None
133
+ try:
134
+ from export_bundle import read_plugin_manifest
135
+ target_version = read_plugin_manifest(Path(__file__).resolve().parents[1]).get("version")
136
+ except Exception:
137
+ pass
138
+ desc_lines = []
139
+ upgrade_options = []
140
+ for idx, row in enumerate(existing_targets):
141
+ h_name = row.get("host")
142
+ label = next((en for name, en, _ in HOSTS if name == h_name), h_name)
143
+ zh_label = next((zh for name, _, zh in HOSTS if name == h_name), h_name)
144
+ details = []
145
+ for k in ("scope", "project", "profile"):
146
+ if row.get(k):
147
+ details.append(k + "=" + str(row[k]))
148
+ ver = row.get("version")
149
+ ver_str = (" (v" + ver + ")") if ver else ""
150
+ det_str = (" [" + ", ".join(details) + "]") if details else ""
151
+ desc_lines.append((label + det_str + ver_str, zh_label + det_str + ver_str))
152
+
153
+ if target_version and ver:
154
+ if target_version != ver.split("+")[0]:
155
+ ver_hint_en = "v%s → v%s (upgrade)" % (ver, target_version)
156
+ ver_hint_zh = "当前 v%s → 目标 v%s(升级覆盖)" % (ver, target_version)
157
+ else:
158
+ ver_hint_en = "v%s (latest) · overwrite" % ver
159
+ ver_hint_zh = "当前已是最新 v%s · 覆盖更新" % ver
160
+ elif ver:
161
+ ver_hint_en = "v%s" % ver
162
+ ver_hint_zh = "当前版本 v%s" % ver
163
+ else:
164
+ ver_hint_en = "installed"
165
+ ver_hint_zh = "已安装"
166
+ upgrade_options.append(Option(
167
+ str(idx),
168
+ label + det_str,
169
+ zh_label + det_str,
170
+ ver_hint_en,
171
+ ver_hint_zh,
172
+ hint_always=True
173
+ ))
174
+
175
+ self.note("Detected existing installations", "检测到已安装宿主", desc_lines)
176
+ host_summary = ", ".join(row.get("host") for row in existing_targets)
177
+ mode = self.select("Installation mode", "请选择操作", [
178
+ Option("upgrade",
179
+ "Upgrade / overwrite installed hosts (%d detected: %s)" % (len(existing_targets), host_summary),
180
+ "升级覆盖已安装宿主(检测到 %d 个:%s)" % (len(existing_targets), host_summary),
181
+ "select which hosts to upgrade, defaults to all",
182
+ "回车默认全选升级,也可自选指定宿主",
183
+ hint_always=True),
184
+ Option("custom",
185
+ "Re-select hosts and customize options from scratch",
186
+ "重新选择宿主与自定义配置",
187
+ "choose hosts and options from scratch",
188
+ "从头重新勾选宿主与参数")
189
+ ], "upgrade")
190
+ if mode == "upgrade":
191
+ if len(existing_targets) == 1:
192
+ chosen = ["0"]
193
+ else:
194
+ chosen = self.multiselect(
195
+ "Select installed hosts to upgrade (Space to toggle, Enter to confirm)",
196
+ "选择要升级覆盖的宿主?(空格 勾选/取消,回车 确认)",
197
+ upgrade_options,
198
+ defaults=[str(i) for i in range(len(existing_targets))]
199
+ )
200
+ targets = [{"host": existing_targets[int(i)]["host"],
201
+ "scope": existing_targets[int(i)].get("scope"),
202
+ "project": existing_targets[int(i)].get("project"),
203
+ "profile": existing_targets[int(i)].get("profile")}
204
+ for i in chosen]
205
+ lines = []
206
+ for target in targets:
207
+ label = next((en for name, en, _ in HOSTS if name == target["host"]), target["host"])
208
+ detail = ", ".join(key + "=" + str(target[key]) for key in ("scope", "project", "profile") if target.get(key))
209
+ lines.append((label + (": " + detail if detail else ""), label + (":" + detail if detail else "")))
210
+ self.note("Upgrade plan", "升级覆盖计划", lines)
211
+ if client["source"] == "desktop" and any(target["host"] == "dsh" for target in targets):
212
+ self.note("DeepSeek Harness (dsh) CLI", "DeepSeek Harness (dsh) CLI", [(client["binary"], client["binary"])])
213
+ if not self.confirm("Upgrade now?", "现在升级覆盖?", True):
214
+ if not self.confirm("Re-select hosts and options?", "重新选择宿主与选项?", True):
215
+ raise KeyboardInterrupt
216
+ else:
217
+ first = targets[0]
218
+ args.host, args.scope, args.project, args.profile = first["host"], first.get("scope"), first.get("project"), first.get("profile")
219
+ return targets
220
+ while True:
221
+ if requested:
222
+ hosts = list(dict.fromkeys(requested))
223
+ else:
224
+ found = {name: bool(shutil.which(getattr(args, name, None) or name)) for name, _, _ in HOSTS}
225
+ hints = {name: ("", None) if found[name] else ("CLI not found", "未找到 CLI") for name in found}
226
+ found["dsh"], hints["dsh"] = client["usable"], _dsh_hint(client)
227
+ if not any(found.values()):
228
+ self.warn("No supported host CLI (codex, claude, dsh) was found on PATH.",
229
+ "PATH 中未找到任何受支持的宿主 CLI(codex、claude、dsh)。")
230
+ raise ValueError("No host CLI found; install Codex, Claude Code or DeepSeek Harness (dsh), "
231
+ "or pass --codex/--claude/--dsh PATH to point at its executable")
232
+ preselected = [row["host"] for row in (existing_targets or []) if found.get(row.get("host"))] or [name for name, _, _ in HOSTS if found[name]]
233
+ hosts = self.multiselect("Install into which hosts? (Space to toggle, Enter to confirm)",
234
+ "安装到哪些宿主?(空格 勾选/取消,回车 确认)", [
235
+ Option(name, en, zh, *hints[name], disabled=not found[name]) for name, en, zh in HOSTS],
236
+ preselected)
237
+ # A carrier that ships its own pnpm is the Desktop CLI, however it was found:
238
+ # only that CLI may manage the profile name Desktop reserves for itself.
239
+ desktop = client["bundled_pnpm"] is not None
240
+ targets = []
241
+ go_back = False
242
+ for host in hosts:
243
+ label = next((en for name, en, _ in HOSTS if name == host), host)
244
+ target = {"host": host, "scope": None, "project": None, "profile": None}
245
+ if host == "claude":
246
+ scope = args.scope
247
+ if scope is None:
248
+ options = [Option("user", "This user", "当前用户"), Option("project", "This project", "指定项目"),
249
+ Option("local", "This project, only for me", "指定项目(仅自己)")]
250
+ if not requested:
251
+ options.append(Option("back", "← Back to host selection", "← 返回重新选择宿主"))
252
+ scope = self.select(label + ": installation scope", label + ":安装范围", options, "user")
253
+ if scope == "back":
254
+ go_back = True
255
+ break
256
+ target["scope"] = scope
257
+ if scope in ("project", "local"):
258
+ target["project"] = args.project or self.input(
259
+ label + ": project directory", label + ":项目目录", str(Path.cwd()), _directory_problem)
260
+ elif host == "dsh":
261
+ # Never guess a DSH profile: it selects a distinct composed runtime.
262
+ profile = args.profile or self.dsh_profile(desktop, allow_back=not requested)
263
+ if profile == "back":
264
+ go_back = True
265
+ break
266
+ target["profile"] = profile
267
+ targets.append(target)
268
+ if go_back:
269
+ continue
270
+ lines = []
271
+ for target in targets:
272
+ label = next((en for name, en, _ in HOSTS if name == target["host"]), target["host"])
273
+ detail = ", ".join(key + "=" + target[key] for key in ("scope", "project", "profile") if target[key])
274
+ lines.append((label + (": " + detail if detail else ""), label + (":" + detail if detail else "")))
275
+ self.note("Installation plan", "安装计划", lines)
276
+ if client["source"] == "desktop" and any(target["host"] == "dsh" for target in targets):
277
+ self.note("DeepSeek Harness (dsh) CLI", "DeepSeek Harness (dsh) CLI", [(client["binary"], client["binary"])])
278
+ if not self.confirm("Install now?", "现在安装?", True):
279
+ if not requested and self.confirm("Re-select hosts and options?", "重新选择宿主与选项?", True):
280
+ continue
281
+ raise KeyboardInterrupt
282
+ first = targets[0]
283
+ args.host, args.scope, args.project, args.profile = first["host"], first["scope"], first["project"], first["profile"]
284
+ return targets
285
+
286
+ def dsh_profile(self, desktop, allow_back=False):
173
287
  """Choose an initialized DSH profile, or name a new one.
174
288
 
175
289
  `dsh plugin` initializes a profile on first use, so the profiles on disk
@@ -186,8 +300,12 @@ class Console(Prompter):
186
300
  return self.input(question[0], question[1], "desktop" if desktop else "", check)
187
301
  options = [Option(row["name"], row["name"], row["name"], *_profile_hint(row)) for row in rows]
188
302
  options.append(Option("", "Another profile name…", "其他 profile 名称…"))
303
+ if allow_back:
304
+ options.append(Option("back", "← Back to host selection", "← 返回重新选择宿主"))
189
305
  default = "desktop" if any(row["name"] == "desktop" for row in rows) else None
190
306
  chosen = self.select("Install into which DeepSeek Harness (dsh) profile?", "安装到哪个 DeepSeek Harness (dsh) profile?", options, default)
307
+ if chosen == "back":
308
+ return "back"
191
309
  if chosen:
192
310
  return chosen
193
311
  return self.input(question[0], question[1], "desktop" if desktop else "", check)
package/src/tui.py CHANGED
@@ -449,8 +449,11 @@ class Prompter:
449
449
  while True:
450
450
  lines = self._render_list(question, options, cursor, chosen, True, message)
451
451
  if not message:
452
- lines[-1] = "%s %s" % (self._color("36", "└"), self._color("2", self.text(
453
- "space toggle · a all · enter confirm", "空格 切换 · a 全选 · 回车 确认")))
452
+ shortcut_hint = self.text(
453
+ "[Space] toggle [a] all/none [Enter] confirm",
454
+ "[空格] 勾选/取消 [a] 全选/全不选 [回车] 确认",
455
+ )
456
+ lines[-1] = "%s %s" % (self._color("36", "└"), self._color("36", shortcut_hint))
454
457
  drawn = self._redraw(lines, drawn)
455
458
  key = self._key()
456
459
  message = None