scriptnow-cli 0.3.94__py3-none-any.whl

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.
@@ -0,0 +1,331 @@
1
+ """Version check and self-upgrade for scriptnow-cli.
2
+
3
+ The CLI checks for a newer version on the public GitHub repo
4
+ (quchenchen/scriptnow-cli — the release mirror of this monorepo) at low
5
+ frequency (once per 24h, cached locally, silent on failure) and offers
6
+ `scriptnow self-upgrade` to apply the update after explicit user consent.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import json
12
+ import importlib.util
13
+ import os
14
+ import shutil
15
+ import subprocess
16
+ import sys
17
+ import threading
18
+ import time
19
+ from pathlib import Path
20
+
21
+ import requests
22
+
23
+ from cli_anything.scriptnow import __version__ as VERSION
24
+
25
+ _REMOTE_INIT = (
26
+ "https://raw.githubusercontent.com/quchenchen/scriptnow-cli/main/"
27
+ "cli_anything/scriptnow/__init__.py"
28
+ )
29
+ # 生产源:sn.igeewa.com 托管 CLI wheel / zip / version.txt,作为自动更新的优先来源
30
+ # (直装 wheel,避免 git 部分克隆被网络掐断;GitHub 仅作版本与下载兜底)。
31
+ _REMOTE_VERSION_TXT = "https://sn.igeewa.com/downloads/scriptnow-cli/version.txt"
32
+ _PROD_WHEEL_TMPL = (
33
+ "https://sn.igeewa.com/downloads/scriptnow-cli/scriptnow_cli-{version}-py3-none-any.whl"
34
+ )
35
+ _CHECK_INTERVAL_SECONDS = 24 * 60 * 60
36
+
37
+
38
+ def _version_tuple(value: str) -> tuple[int, int, int] | None:
39
+ parts = value.strip().split(".")
40
+ if len(parts) != 3 or not all(part.isdigit() for part in parts):
41
+ return None
42
+ return tuple(int(part) for part in parts) # type: ignore[return-value]
43
+
44
+
45
+ def is_newer_version(candidate: str, current: str = VERSION) -> bool:
46
+ candidate_tuple = _version_tuple(candidate)
47
+ current_tuple = _version_tuple(current)
48
+ return bool(
49
+ candidate_tuple is not None
50
+ and current_tuple is not None
51
+ and candidate_tuple > current_tuple
52
+ )
53
+
54
+
55
+ def _state_path() -> Path:
56
+ override = os.environ.get("SCRIPTNOW_CLI_CONFIG")
57
+ root = Path(override).parent if override else Path(
58
+ os.environ.get("XDG_CONFIG_HOME", str(Path.home() / ".config"))
59
+ ) / "scriptnow-cli"
60
+ return root / "version-check.json"
61
+
62
+
63
+ def _config_path() -> Path:
64
+ override = os.environ.get("SCRIPTNOW_CLI_CONFIG")
65
+ root = Path(override).parent if override else Path(
66
+ os.environ.get("XDG_CONFIG_HOME", str(Path.home() / ".config"))
67
+ ) / "scriptnow-cli"
68
+ return root / "config.json"
69
+
70
+
71
+ def load_config() -> dict[str, object]:
72
+ """Read the CLI config file. Missing/corrupt → defaults."""
73
+ try:
74
+ data = json.loads(_config_path().read_text(encoding="utf-8"))
75
+ if isinstance(data, dict):
76
+ return data
77
+ except (OSError, ValueError):
78
+ pass
79
+ return {}
80
+
81
+
82
+ def set_config(**updates: object) -> dict[str, object]:
83
+ """Persist config, preserving unknown keys."""
84
+ config = load_config()
85
+ config.update(updates)
86
+ try:
87
+ path = _config_path()
88
+ path.parent.mkdir(parents=True, exist_ok=True)
89
+ path.write_text(json.dumps(config, ensure_ascii=False, indent=2))
90
+ except OSError:
91
+ raise
92
+ return config
93
+
94
+
95
+ def auto_upgrade_enabled() -> bool:
96
+ return bool(load_config().get("autoUpgrade", False))
97
+
98
+
99
+ def _fetch_production_version(timeout: int) -> str | None:
100
+ """Read version.txt hosted on the production download host (preferred)."""
101
+ try:
102
+ response = requests.get(_REMOTE_VERSION_TXT, timeout=timeout)
103
+ if response.status_code != 200:
104
+ return None
105
+ version = response.text.strip()
106
+ if version and all(part.isdigit() for part in version.split(".")[:2]):
107
+ return version
108
+ except requests.RequestException:
109
+ return None
110
+ return None
111
+
112
+
113
+ def _fetch_github_version(timeout: int) -> str | None:
114
+ """Query the GitHub release mirror for the newest __version__ (fallback)."""
115
+ try:
116
+ response = requests.get(_REMOTE_INIT, timeout=timeout)
117
+ if response.status_code != 200:
118
+ return None
119
+ for line in response.text.splitlines():
120
+ line = line.strip()
121
+ if line.startswith("__version__"):
122
+ parts = line.split('"')
123
+ if len(parts) >= 2 and parts[1]:
124
+ return parts[1]
125
+ except requests.RequestException:
126
+ return None
127
+ return None
128
+
129
+
130
+ def latest_version(timeout: int = 8) -> str | None:
131
+ """Return the newest CLI version — production host first, GitHub fallback.
132
+
133
+ Returns None when all sources fail or the remote version cannot be parsed;
134
+ the caller treats that as "no information" and stays silent.
135
+ """
136
+ version = _fetch_production_version(timeout)
137
+ if version:
138
+ return version
139
+ return _fetch_github_version(timeout)
140
+
141
+
142
+ def _is_stale() -> bool:
143
+ try:
144
+ state = json.loads(_state_path().read_text(encoding="utf-8"))
145
+ return float(state.get("checked_at") or 0) + _CHECK_INTERVAL_SECONDS < time.time()
146
+ except (OSError, ValueError, KeyError):
147
+ return True
148
+
149
+
150
+ def _record_check() -> None:
151
+ try:
152
+ path = _state_path()
153
+ path.parent.mkdir(parents=True, exist_ok=True)
154
+ path.write_text(json.dumps({"checked_at": int(time.time())}))
155
+ except OSError:
156
+ pass
157
+
158
+
159
+ def check_for_update(force: bool = False) -> str | None:
160
+ """Return the latest version when a newer one exists, else None.
161
+
162
+ Non-blocking for the caller: network happens in this function with a
163
+ short timeout; the 24h cache avoids a request on every invocation.
164
+ """
165
+ if not force and not _is_stale():
166
+ return None
167
+ latest = latest_version()
168
+ _record_check()
169
+ if latest is None or not is_newer_version(latest):
170
+ return None
171
+ return latest
172
+
173
+
174
+ def _environment_install_command(
175
+ source: str, *, upgrade_only: bool = False
176
+ ) -> tuple[str, list[str]] | None:
177
+ """Install into the interpreter that is running this CLI.
178
+
179
+ Virtual environments must never receive ``--user``. A base interpreter may
180
+ need a user install; ``--break-system-packages`` is POSIX-only. If a uv-made
181
+ environment has no pip module, target it explicitly through ``uv pip``.
182
+ """
183
+ action = "--upgrade" if upgrade_only else "--force-reinstall"
184
+ if importlib.util.find_spec("pip") is not None:
185
+ flags: list[str] = []
186
+ if sys.prefix == getattr(sys, "base_prefix", sys.prefix):
187
+ flags.append("--user")
188
+ if os.name != "nt":
189
+ flags.append("--break-system-packages")
190
+ return sys.executable, ["-m", "pip", "install", *flags, action, source]
191
+ if shutil.which("uv"):
192
+ return "uv", ["pip", "install", "--python", sys.executable, action, source]
193
+ return None
194
+
195
+
196
+ def _install_command(version: str | None = None) -> tuple[str, list[str]] | None:
197
+ """Resolve an upgrade command for the current installed environment."""
198
+ try:
199
+ import importlib.metadata as md
200
+
201
+ dist = md.distribution("scriptnow-cli")
202
+ except md.PackageNotFoundError:
203
+ return None
204
+ # Editable install (local dev): can't self-upgrade blindly — tell the user.
205
+ for file in dist.files or []:
206
+ if "__editable__" in str(file):
207
+ return None
208
+ source = (
209
+ _PROD_WHEEL_TMPL.format(version=version)
210
+ if version
211
+ else "https://codeload.github.com/quchenchen/scriptnow-cli/tar.gz/refs/heads/main"
212
+ )
213
+ return _environment_install_command(source)
214
+
215
+
216
+ def is_editable_install() -> bool:
217
+ try:
218
+ import importlib.metadata as md
219
+
220
+ dist = md.distribution("scriptnow-cli")
221
+ except md.PackageNotFoundError:
222
+ return False
223
+ return any("__editable__" in str(file) for file in (dist.files or []))
224
+
225
+
226
+ def _upgrade_fallback() -> tuple[str, list[str]] | None:
227
+ """备选升级命令:git+https(当 codeload 被拦时)。"""
228
+ return _environment_install_command(
229
+ "git+https://github.com/quchenchen/scriptnow-cli.git", upgrade_only=True
230
+ )
231
+
232
+
233
+ def upgrade(quiet: bool = False) -> bool:
234
+ """Apply the upgrade. Returns True on success.
235
+
236
+ Attempts sources in priority order: production wheel (sn.igeewa.com) →
237
+ codeload tar.gz (GitHub) → git+https (last resort)."""
238
+ latest = latest_version()
239
+ if latest is not None and not is_newer_version(latest) and latest != VERSION:
240
+ return True # Never downgrade a newer local/dev build to an older feed.
241
+ if is_editable_install():
242
+ target = latest or VERSION
243
+ candidate = _environment_install_command(
244
+ _PROD_WHEEL_TMPL.format(version=target)
245
+ )
246
+ if candidate is None:
247
+ if not quiet:
248
+ print("当前 Python 环境无 pip 且未找到 uv,无法自动升级。")
249
+ return False
250
+ result = subprocess.run(
251
+ [candidate[0], *candidate[1]],
252
+ capture_output=True,
253
+ text=True,
254
+ timeout=300,
255
+ )
256
+ if result.returncode == 0:
257
+ return True
258
+ if not quiet:
259
+ print((result.stderr or "同版本补丁刷新失败。")[-500:])
260
+ return False
261
+ attempts: list[tuple[str, list[str]] | None] = []
262
+ if latest:
263
+ attempts.append(_install_command(latest)) # 生产源 wheel
264
+ attempts.append(_install_command()) # codeload main 兜底
265
+ attempts.append(_upgrade_fallback()) # git+https 最后兜底
266
+ last_stderr = ""
267
+ for candidate in attempts:
268
+ if not candidate:
269
+ continue
270
+ cmd = [candidate[0], *candidate[1]]
271
+ result = subprocess.run(cmd, capture_output=True, text=True, timeout=300)
272
+ if result.returncode == 0:
273
+ return True
274
+ last_stderr = result.stderr or ""
275
+ if not quiet:
276
+ print(last_stderr[-500:] or "升级失败。")
277
+ return False
278
+
279
+
280
+ def maybe_warn_in_background() -> None:
281
+ """Spawn a non-blocking background version check.
282
+
283
+ Default behaviour: print a one-line hint when a newer version exists.
284
+
285
+ When the user has opted into ``autoUpgrade`` (``scriptnow config
286
+ auto-upgrade on``), the background check instead attempts the upgrade
287
+ automatically and notifies the user before/after — never blocking the
288
+ main command. Editable/dev installs are never auto-upgraded.
289
+ """
290
+ import sys
291
+
292
+ def _run() -> None:
293
+ try:
294
+ latest = check_for_update()
295
+ if not latest:
296
+ return
297
+ if auto_upgrade_enabled():
298
+ if _install_command() is None:
299
+ # Editable/dev install: never auto-upgrade; fall back to a
300
+ # normal hint so the user knows a newer version exists.
301
+ print(
302
+ f"发现 ScriptNow CLI 新版本 v{latest}(当前 v{VERSION})。"
303
+ f"本地为开发安装,请手动升级。",
304
+ file=sys.stderr,
305
+ )
306
+ return
307
+ print(
308
+ f"[scriptnow] 检测到新版本 v{latest}(当前 v{VERSION}),正在自动升级…",
309
+ file=sys.stderr,
310
+ )
311
+ ok = upgrade(quiet=False)
312
+ if ok:
313
+ print(
314
+ f"[scriptnow] 已自动升级到 v{latest}。请重新运行 scriptnow 使新版本生效。",
315
+ file=sys.stderr,
316
+ )
317
+ else:
318
+ print(
319
+ "[scriptnow] 自动升级未完成,请运行 `scriptnow self-upgrade` 手动升级。",
320
+ file=sys.stderr,
321
+ )
322
+ return
323
+ print(
324
+ f"发现 ScriptNow CLI 新版本 v{latest}(当前 v{VERSION})。"
325
+ f"运行 `scriptnow self-upgrade` 自动升级,或 `scriptnow version --check` 查看。",
326
+ file=sys.stderr,
327
+ )
328
+ except Exception:
329
+ pass # never break the main command because of a version hint
330
+
331
+ threading.Thread(target=_run, daemon=True).start()
@@ -0,0 +1,88 @@
1
+ Metadata-Version: 2.4
2
+ Name: scriptnow-cli
3
+ Version: 0.3.94
4
+ Summary: ScriptNow 创作 CLI —— 从灵感到成书交付的一站式命令行(CLI-Anything 模式)
5
+ Author: ScriptNow
6
+ License: MIT
7
+ Project-URL: Homepage, https://sn.igeewa.com
8
+ Project-URL: Documentation, https://sn.igeewa.com/cli
9
+ Project-URL: Repository, https://github.com/quchenchen/scriptnow-cli
10
+ Project-URL: Issues, https://github.com/quchenchen/scriptnow-cli/issues
11
+ Keywords: scriptnow,novel,script,cli,agent,cli-anything
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Environment :: Console
14
+ Classifier: Intended Audience :: End Users/Desktop
15
+ Classifier: Topic :: Text Processing
16
+ Requires-Python: >=3.10
17
+ Description-Content-Type: text/markdown
18
+ License-File: LICENSE
19
+ Requires-Dist: click>=8.0
20
+ Requires-Dist: requests>=2.28
21
+ Dynamic: license-file
22
+ Dynamic: requires-python
23
+
24
+ # ScriptNow CLI
25
+
26
+ An agent-friendly command-line client for the [ScriptNow](https://sn.igeewa.com)
27
+ creative writing platform. Authors and screenwriters can work with an AI agent
28
+ to develop projects, review planning candidates, write chapters or scenes, and
29
+ export their work.
30
+
31
+ ## Install
32
+
33
+ Requires Python 3.10 or later. Use a virtual environment or a CLI tool installer:
34
+
35
+ ```sh
36
+ pipx install scriptnow-cli
37
+ # Alternatively, inside a virtual environment:
38
+ python -m pip install scriptnow-cli
39
+ ```
40
+
41
+ The command is `scriptnow`; the distribution name is `scriptnow-cli`.
42
+
43
+ ## Get started
44
+
45
+ ```sh
46
+ scriptnow --version
47
+ scriptnow agent-guide --json
48
+ scriptnow doctor
49
+ scriptnow login --host https://sn.igeewa.com --email you@example.com
50
+ scriptnow guide --medium novel
51
+ # For screenwriting:
52
+ scriptnow guide --medium script
53
+ ```
54
+
55
+ Enter your password at the hidden terminal prompt. A ScriptNow account and
56
+ appropriate project access are required; installing this client does not create
57
+ an account or grant service credits.
58
+
59
+ ## For AI agents
60
+
61
+ Always read `scriptnow agent-guide --json` before operating the platform. Use
62
+ the current command's `--help` for arguments and schemas. Read platform state
63
+ before writes and read it back after success. Do not invent project IDs or
64
+ treat local drafts as saved platform content.
65
+
66
+ - Create each author's own project and retain the ID returned by the platform.
67
+ - Co-create planning locally, submit candidates through the appropriate
68
+ `propose` commands, and obtain the author's explicit decision before adoption.
69
+ - Follow direction, story core, blueprint, synopsis, rough outline, chapter or
70
+ episode outlines, then prose. Check planning quality and writing readiness.
71
+ - By default, invoke the platform's writing agent for chapters and scenes.
72
+ Follow the returned run ID; do not repeatedly start the same generation.
73
+ - Show complete candidate content and record the human's actual decision using
74
+ the current review protocol. Never infer adoption from silence.
75
+
76
+ Novels and scripts use separate domain commands and formats. The platform is
77
+ the source of truth for adopted content and continuity.
78
+
79
+ ## Distribution and updates
80
+
81
+ The platform also distributes versioned wheels and Windows installers at
82
+ [its download host](https://sn.igeewa.com/downloads/scriptnow-cli/).
83
+ `scriptnow self-upgrade` checks the platform distribution first; GitHub is a
84
+ fallback. PyPI releases may appear on a different schedule. Automatic updates
85
+ are opt-in with `scriptnow config on`.
86
+
87
+ See the [full CLI documentation](https://github.com/quchenchen/scriptnow-cli#readme)
88
+ and [platform guide](https://sn.igeewa.com/cli). Licensed under MIT.
@@ -0,0 +1,17 @@
1
+ cli_anything/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
+ cli_anything/scriptnow/README.md,sha256=OBSwDAVHHl-25Po9HxsEUsWdZGsYofshzQytO6vBGzw,30270
3
+ cli_anything/scriptnow/__init__.py,sha256=6JRriDERwvidfcaumV2DT6siP0TAadgDb8r6tcy20UA,102
4
+ cli_anything/scriptnow/__main__.py,sha256=TFNjbBI8UK64s6E1mvCATanTOKPwxr9038L9u8HAoYk,147
5
+ cli_anything/scriptnow/scriptnow_cli.py,sha256=ABwcNFwmGjrL-tEdZOvecB7ain9G_wZO7Oc-syp_cVM,532182
6
+ cli_anything/scriptnow/ui.py,sha256=kVl7O0sg65YJTL1MCzwX1WTT2zivCoLVrbyA-Irrqpg,4982
7
+ cli_anything/scriptnow/skills/SKILL.md,sha256=p_lbTcakQKvKdZHuhcbpQkHCFHtbL9zYLQ8cjS825Vg,23696
8
+ cli_anything/scriptnow/utils/__init__.py,sha256=pFMGHVnbNf7ROI_ERG9lRSEdlyvfLBvJI1UOeKUGDyE,54
9
+ cli_anything/scriptnow/utils/diag.py,sha256=h_m3NFL-Vy35ALVs9tAUS4UjbthqqN0cjnQsr21Sw-Y,5238
10
+ cli_anything/scriptnow/utils/session.py,sha256=MUJGswWMpVeL4cyAqS87VPJCHTB9rCAuQcBWzV6Arrc,22014
11
+ cli_anything/scriptnow/utils/upgrade.py,sha256=NyM-L0B_w2tfrp6Jznk53HFPBbMrvoulUCGO2jBOzPY,11861
12
+ scriptnow_cli-0.3.94.dist-info/licenses/LICENSE,sha256=1p07s14BpEHskGKRoVRrNWSjCCaN6QQx1pGSRtNIQ9w,1066
13
+ scriptnow_cli-0.3.94.dist-info/METADATA,sha256=qxrxZ-DnTGQV4jn3aOgY6qr_IsbU0DtXTyoJqS7O7Vs,3491
14
+ scriptnow_cli-0.3.94.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
15
+ scriptnow_cli-0.3.94.dist-info/entry_points.txt,sha256=b_T8LoAoitfuSI2-8x-taBd7Umklo1n5s4GF-1t-MLI,72
16
+ scriptnow_cli-0.3.94.dist-info/top_level.txt,sha256=LI1GTe19xehXrxQtg-3ltETALXYkoctC4Y_iuDiCSRo,13
17
+ scriptnow_cli-0.3.94.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (84.0.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ scriptnow = cli_anything.scriptnow.scriptnow_cli:main
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 ScriptNow
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.
@@ -0,0 +1 @@
1
+ cli_anything