indesign-cli 0.2.0__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.
Files changed (72) hide show
  1. cli_anything/indesign/README.md +32 -0
  2. cli_anything/indesign/__init__.py +1 -0
  3. cli_anything/indesign/__main__.py +5 -0
  4. cli_anything/indesign/core/artifacts.py +57 -0
  5. cli_anything/indesign/core/catalog.py +405 -0
  6. cli_anything/indesign/core/domains.py +178 -0
  7. cli_anything/indesign/core/envelope.py +65 -0
  8. cli_anything/indesign/core/errors.py +30 -0
  9. cli_anything/indesign/core/health.py +46 -0
  10. cli_anything/indesign/core/hidden_backend.py +116 -0
  11. cli_anything/indesign/core/hidden_handler_schemas.py +223 -0
  12. cli_anything/indesign/core/mcp_backend.py +152 -0
  13. cli_anything/indesign/core/node_setup.py +35 -0
  14. cli_anything/indesign/core/paths.py +41 -0
  15. cli_anything/indesign/core/plugins/__init__.py +2 -0
  16. cli_anything/indesign/core/plugins/backend.py +90 -0
  17. cli_anything/indesign/core/plugins/discovery.py +69 -0
  18. cli_anything/indesign/core/plugins/host_actions.py +76 -0
  19. cli_anything/indesign/core/plugins/install.py +38 -0
  20. cli_anything/indesign/core/plugins/manifest.py +279 -0
  21. cli_anything/indesign/core/plugins/validate.py +181 -0
  22. cli_anything/indesign/core/router.py +217 -0
  23. cli_anything/indesign/core/runtime.py +59 -0
  24. cli_anything/indesign/core/scripts.py +44 -0
  25. cli_anything/indesign/core/session.py +68 -0
  26. cli_anything/indesign/indesign_cli.py +320 -0
  27. cli_anything/indesign/node/hidden_handler_bridge.mjs +111 -0
  28. cli_anything/indesign/server/package-lock.json +168 -0
  29. cli_anything/indesign/server/package.json +45 -0
  30. cli_anything/indesign/server/src/advanced/index.js +76 -0
  31. cli_anything/indesign/server/src/core/InDesignMCPServer.js +273 -0
  32. cli_anything/indesign/server/src/core/scriptExecutor.js +271 -0
  33. cli_anything/indesign/server/src/core/sessionManager.js +545 -0
  34. cli_anything/indesign/server/src/handlers/advancedTemplateHandlers.js +1072 -0
  35. cli_anything/indesign/server/src/handlers/bookHandlers.js +490 -0
  36. cli_anything/indesign/server/src/handlers/documentHandlers.js +1472 -0
  37. cli_anything/indesign/server/src/handlers/exportHandlers.js +208 -0
  38. cli_anything/indesign/server/src/handlers/graphicsHandlers.js +605 -0
  39. cli_anything/indesign/server/src/handlers/groupHandlers.js +358 -0
  40. cli_anything/indesign/server/src/handlers/helpHandlers.js +347 -0
  41. cli_anything/indesign/server/src/handlers/index.js +77 -0
  42. cli_anything/indesign/server/src/handlers/layerHandlers.js +75 -0
  43. cli_anything/indesign/server/src/handlers/masterSpreadHandlers.js +451 -0
  44. cli_anything/indesign/server/src/handlers/pageHandlers.js +698 -0
  45. cli_anything/indesign/server/src/handlers/pageItemHandlers.js +704 -0
  46. cli_anything/indesign/server/src/handlers/presentationHandlers.js +220 -0
  47. cli_anything/indesign/server/src/handlers/spreadHandlers.js +348 -0
  48. cli_anything/indesign/server/src/handlers/styleHandlers.js +458 -0
  49. cli_anything/indesign/server/src/handlers/textHandlers.js +431 -0
  50. cli_anything/indesign/server/src/handlers/utilityHandlers.js +83 -0
  51. cli_anything/indesign/server/src/index.js +17 -0
  52. cli_anything/indesign/server/src/types/index.js +106 -0
  53. cli_anything/indesign/server/src/types/toolDefinitionsAdvancedTemplates.js +144 -0
  54. cli_anything/indesign/server/src/types/toolDefinitionsBook.js +224 -0
  55. cli_anything/indesign/server/src/types/toolDefinitionsContent.js +353 -0
  56. cli_anything/indesign/server/src/types/toolDefinitionsDocument.js +409 -0
  57. cli_anything/indesign/server/src/types/toolDefinitionsExport.js +65 -0
  58. cli_anything/indesign/server/src/types/toolDefinitionsLayer.js +40 -0
  59. cli_anything/indesign/server/src/types/toolDefinitionsMasterSpread.js +160 -0
  60. cli_anything/indesign/server/src/types/toolDefinitionsPage.js +271 -0
  61. cli_anything/indesign/server/src/types/toolDefinitionsPageItemGroup.js +437 -0
  62. cli_anything/indesign/server/src/types/toolDefinitionsPresentation.js +83 -0
  63. cli_anything/indesign/server/src/types/toolDefinitionsSpread.js +158 -0
  64. cli_anything/indesign/server/src/types/toolDefinitionsUtility.js +40 -0
  65. cli_anything/indesign/server/src/utils/stringUtils.js +107 -0
  66. cli_anything/indesign/skills/SKILL.md +198 -0
  67. indesign_cli-0.2.0.dist-info/METADATA +267 -0
  68. indesign_cli-0.2.0.dist-info/RECORD +72 -0
  69. indesign_cli-0.2.0.dist-info/WHEEL +5 -0
  70. indesign_cli-0.2.0.dist-info/entry_points.txt +3 -0
  71. indesign_cli-0.2.0.dist-info/licenses/LICENSE +21 -0
  72. indesign_cli-0.2.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,181 @@
1
+ from __future__ import annotations
2
+
3
+ import shutil
4
+ import subprocess
5
+ from pathlib import Path
6
+ from typing import Any
7
+
8
+ from ..errors import CliError
9
+ from .backend import PluginBackend
10
+ from .discovery import discover_plugins
11
+ from .manifest import (
12
+ TOOL_ID_PATTERN,
13
+ PluginRecord,
14
+ load_plugin_record,
15
+ validate_manifest,
16
+ )
17
+
18
+
19
+ REQUIRED_TOOL_FIELDS = {
20
+ "id",
21
+ "domain",
22
+ "name",
23
+ "one_line_purpose",
24
+ "arg_names",
25
+ "rank",
26
+ "schema_size",
27
+ "callable",
28
+ "requires",
29
+ "side_effects",
30
+ "artifact_kinds",
31
+ "destructive",
32
+ "target_scope",
33
+ "needs_indesign",
34
+ "produces_artifacts",
35
+ }
36
+
37
+
38
+ def _error(code: str, message: str, **details: Any) -> dict[str, Any]:
39
+ return {"code": code, "message": message, "details": details}
40
+
41
+
42
+ def _schema_errors(tool: dict[str, Any], schema_payload: dict[str, Any]) -> list[dict[str, Any]]:
43
+ errors: list[dict[str, Any]] = []
44
+ schema = schema_payload.get("inputSchema")
45
+ tool_id = str(tool.get("id"))
46
+ if not isinstance(schema, dict):
47
+ return [_error("PLUGIN_SCHEMA_INVALID", "inputSchema must be an object", tool_id=tool_id)]
48
+ if schema.get("type") != "object":
49
+ errors.append(_error("PLUGIN_SCHEMA_INVALID", "inputSchema.type must be object", tool_id=tool_id))
50
+ properties = schema.get("properties")
51
+ if not isinstance(properties, dict):
52
+ errors.append(_error("PLUGIN_SCHEMA_INVALID", "inputSchema.properties must be an object", tool_id=tool_id))
53
+ properties = {}
54
+ for arg_name in tool.get("arg_names", []):
55
+ if arg_name not in properties:
56
+ errors.append(_error("PLUGIN_SCHEMA_INVALID", "arg_names must match inputSchema.properties", tool_id=tool_id, argument=arg_name))
57
+ for required in schema.get("required", []):
58
+ if required not in properties:
59
+ errors.append(_error("PLUGIN_SCHEMA_INVALID", "required argument missing from properties", tool_id=tool_id, argument=required))
60
+ return errors
61
+
62
+
63
+ def _tool_errors(record: PluginRecord, tool: dict[str, Any]) -> list[dict[str, Any]]:
64
+ errors: list[dict[str, Any]] = []
65
+ missing = sorted(field for field in REQUIRED_TOOL_FIELDS if field not in tool)
66
+ for field in missing:
67
+ errors.append(_error("PLUGIN_TOOL_INVALID", "Missing tool field", tool_id=tool.get("id"), field=field))
68
+ if missing:
69
+ return errors
70
+ tool_id = tool["id"]
71
+ if not isinstance(tool_id, str) or not TOOL_ID_PATTERN.match(tool_id):
72
+ errors.append(_error("PLUGIN_TOOL_INVALID", "Invalid tool id", tool_id=tool_id))
73
+ elif not tool_id.startswith(f"{record.domain}."):
74
+ errors.append(_error("PLUGIN_TOOL_INVALID", "Tool id must use plugin domain", tool_id=tool_id, domain=record.domain))
75
+ if tool.get("domain") != record.domain:
76
+ errors.append(_error("PLUGIN_TOOL_INVALID", "Tool domain must match plugin domain", tool_id=tool_id, domain=tool.get("domain")))
77
+ for list_field in ("arg_names", "requires", "side_effects", "artifact_kinds"):
78
+ if not isinstance(tool.get(list_field), list):
79
+ errors.append(_error("PLUGIN_TOOL_INVALID", f"{list_field} must be an array", tool_id=tool_id))
80
+ for bool_field in ("callable", "destructive", "needs_indesign", "produces_artifacts"):
81
+ if not isinstance(tool.get(bool_field), bool):
82
+ errors.append(_error("PLUGIN_TOOL_INVALID", f"{bool_field} must be boolean", tool_id=tool_id))
83
+ return errors
84
+
85
+
86
+ def validate_plugin_path(path_value: str, *, host_version: str) -> dict[str, Any]:
87
+ errors: list[dict[str, Any]] = []
88
+ warnings: list[dict[str, Any]] = []
89
+ try:
90
+ record = load_plugin_record(Path(path_value), source="validate", host_version=host_version)
91
+ except CliError as exc:
92
+ manifest_errors = exc.details.get("errors") if isinstance(exc.details.get("errors"), list) else None
93
+ errors.extend(manifest_errors or [_error(exc.code, exc.message, **exc.details)])
94
+ return {"ok": False, "plugin": None, "errors": errors, "warnings": warnings, "summary": {}}
95
+
96
+ manifest_errors = validate_manifest(
97
+ manifest=record.manifest,
98
+ root=record.root,
99
+ manifest_path=record.manifest_path,
100
+ host_version=host_version,
101
+ )
102
+ errors.extend(manifest_errors)
103
+ if errors:
104
+ return {"ok": False, "plugin": record.id, "errors": errors, "warnings": warnings, "summary": {}}
105
+
106
+ backend = PluginBackend(record)
107
+ tools: list[dict[str, Any]] = []
108
+ try:
109
+ handshake = backend.handshake({"name": "indesign-cli", "version": host_version, "protocol": "indesign-cli-plugin.v1"})
110
+ if handshake.get("id") != record.id:
111
+ errors.append(_error("PLUGIN_HANDSHAKE_FAILED", "Handshake id does not match manifest", expected=record.id, actual=handshake.get("id")))
112
+ if handshake.get("protocol") != record.manifest["protocol"]:
113
+ errors.append(_error("PLUGIN_HANDSHAKE_FAILED", "Handshake protocol does not match manifest", expected=record.manifest["protocol"], actual=handshake.get("protocol")))
114
+ tools = backend.list_tools()
115
+ except CliError as exc:
116
+ errors.append(_error(exc.code, exc.message, **exc.details))
117
+
118
+ for tool in tools:
119
+ errors.extend(_tool_errors(record, tool))
120
+ if tool.get("callable") and isinstance(tool.get("id"), str):
121
+ try:
122
+ schema_payload = backend.schema(tool["id"])
123
+ except CliError as exc:
124
+ errors.append(_error(exc.code, exc.message, tool_id=tool["id"], **exc.details))
125
+ continue
126
+ errors.extend(_schema_errors(tool, schema_payload))
127
+
128
+ return {
129
+ "ok": not errors,
130
+ "plugin": record.id,
131
+ "errors": errors,
132
+ "warnings": warnings,
133
+ "summary": {
134
+ "tools": len(tools),
135
+ "needs_indesign": sum(1 for tool in tools if tool.get("needs_indesign")),
136
+ "host_actions": record.manifest.get("capabilities", {}).get("host_actions", []),
137
+ },
138
+ }
139
+
140
+
141
+ def doctor_plugin(plugin_id: str, *, cwd: Path, host_version: str, deep: bool = False) -> dict[str, Any]:
142
+ records, discovery_warnings = discover_plugins(cwd, host_version=host_version)
143
+ record = next((item for item in records if item.id == plugin_id), None)
144
+ if not record:
145
+ raise CliError("Plugin is not installed", code="PLUGIN_NOT_INSTALLED", details={"id": plugin_id})
146
+
147
+ checks: list[dict[str, Any]] = []
148
+ checks.append({"name": "discovered", "ok": True, "source": record.source})
149
+ session_dir = cwd / ".indesign-cli"
150
+ try:
151
+ session_dir.mkdir(parents=True, exist_ok=True)
152
+ probe = session_dir / ".doctor-write-test"
153
+ probe.write_text("ok", encoding="utf-8")
154
+ probe.unlink()
155
+ checks.append({"name": "session_writable", "ok": True})
156
+ except OSError as exc:
157
+ checks.append({"name": "session_writable", "ok": False, "message": str(exc)})
158
+
159
+ node = shutil.which("node")
160
+ checks.append({"name": "node_available", "ok": node is not None, "path": node})
161
+ if node:
162
+ completed = subprocess.run(["node", "--version"], text=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, check=False)
163
+ checks.append({"name": "node_version", "ok": completed.returncode == 0, "version": completed.stdout.strip()})
164
+
165
+ validate_payload = validate_plugin_path(str(record.root), host_version=host_version)
166
+ checks.append({"name": "validate", "ok": validate_payload["ok"], "errors": validate_payload["errors"]})
167
+
168
+ try:
169
+ plugin_doctor = PluginBackend(record).doctor()
170
+ checks.append({"name": "plugin_doctor", "ok": bool(plugin_doctor.get("ok", True)), "data": plugin_doctor})
171
+ except CliError as exc:
172
+ checks.append({"name": "plugin_doctor", "ok": False, "code": exc.code, "message": exc.message})
173
+
174
+ return {
175
+ "ok": all(check.get("ok") is True for check in checks),
176
+ "plugin": record.id,
177
+ "source": record.source,
178
+ "deep": deep,
179
+ "checks": checks,
180
+ "warnings": discovery_warnings,
181
+ }
@@ -0,0 +1,217 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import sys
5
+ from json import JSONDecodeError
6
+ from pathlib import Path
7
+ from typing import Any
8
+
9
+ from .catalog import Catalog
10
+ from .errors import CliError
11
+ from .hidden_backend import HiddenHandlerBackend
12
+ from .mcp_backend import McpBackend
13
+ from .plugins.backend import PluginBackend
14
+ from .plugins.host_actions import ALLOWED_HOST_ACTIONS, HostActionExecutor
15
+
16
+
17
+ BACKENDS = {
18
+ "advanced": "src/advanced/index.js",
19
+ "classic": "src/index.js",
20
+ }
21
+
22
+
23
+ PRIMITIVE_SCHEMAS = {
24
+ "export.verify": {
25
+ "type": "object",
26
+ "additionalProperties": False,
27
+ "properties": {
28
+ "path": {"type": "string", "description": "要验证的 PDF 或 IDML 路径"},
29
+ "created_after": {"type": "string", "description": "ISO 时间戳;用于避免验证到旧产物"},
30
+ },
31
+ "required": ["path"],
32
+ },
33
+ "server.health": {
34
+ "type": "object",
35
+ "additionalProperties": False,
36
+ "properties": {
37
+ "deep": {"type": "boolean", "description": "是否执行较深的依赖检查"},
38
+ },
39
+ },
40
+ "server.setup": {"type": "object", "additionalProperties": False, "properties": {}},
41
+ "session.show": {
42
+ "type": "object",
43
+ "additionalProperties": False,
44
+ "properties": {
45
+ "verbose": {"type": "boolean", "description": "是否显示允许展示的详细 session 信息"},
46
+ },
47
+ },
48
+ "session.clear": {"type": "object", "additionalProperties": False, "properties": {}},
49
+ "script.run": {
50
+ "type": "object",
51
+ "additionalProperties": False,
52
+ "properties": {
53
+ "file": {"type": "string", "description": "要执行的 JSX 文件路径"},
54
+ "stdin": {"type": "boolean", "description": "从 stdin 读取临时 JSX"},
55
+ "timeout": {"type": "integer", "description": "脚本通道超时秒数,范围 1-3600"},
56
+ },
57
+ },
58
+ "skill.install": {
59
+ "type": "object",
60
+ "additionalProperties": False,
61
+ "properties": {
62
+ "target": {"type": "string", "description": "目标项目根目录;默认使用当前工作目录"},
63
+ },
64
+ },
65
+ }
66
+
67
+
68
+ class Router:
69
+ def __init__(self, catalog: Catalog, repo_root: Path, backend_timeout_seconds: int | None = None) -> None:
70
+ self.catalog = catalog
71
+ self.repo_root = repo_root
72
+ self.backend_timeout_seconds = backend_timeout_seconds
73
+
74
+ def _find(self, tool_id: str) -> dict[str, Any]:
75
+ matches = [tool for tool in self.catalog.list_tools(callable_only=False) if tool["id"] == tool_id]
76
+ if not matches:
77
+ raise CliError(f"Tool not found: {tool_id}", code="TOOL_NOT_FOUND")
78
+ return matches[0]
79
+
80
+ def schema(self, tool_id: str) -> dict[str, Any]:
81
+ tool = self._find(tool_id)
82
+ if not tool["callable"]:
83
+ raise CliError(f"Tool is not callable: {tool_id}", code="TOOL_NOT_CALLABLE")
84
+ if tool["source"] in {"cli", "script"}:
85
+ return {"tool": tool, "inputSchema": PRIMITIVE_SCHEMAS.get(tool_id, {"type": "object", "properties": {}})}
86
+ if tool["source"] == "hidden_handler":
87
+ return {"tool": tool, "inputSchema": HiddenHandlerBackend(self.repo_root).schema(tool_id)}
88
+ if tool["source"] == "plugin":
89
+ backend = self._plugin_backend(tool)
90
+ payload = backend.schema(tool_id)
91
+ return {"tool": tool, "inputSchema": payload.get("inputSchema", {})}
92
+ backend = self._backend(tool["source"])
93
+ for item in backend.list_tools():
94
+ if item["name"] == tool["name"]:
95
+ return {"tool": tool, "inputSchema": item.get("inputSchema", {})}
96
+ raise CliError(f"Backend schema missing for {tool_id}", code="SCHEMA_NOT_FOUND")
97
+
98
+ def call(self, tool_id: str, args: dict[str, Any]) -> dict[str, Any]:
99
+ tool = self._find(tool_id)
100
+ if not tool["callable"]:
101
+ raise CliError(f"Tool is not callable: {tool_id}", code="TOOL_NOT_CALLABLE")
102
+ if tool["source"] == "cli":
103
+ return self._call_cli_primitive(tool_id, args)
104
+ if tool["source"] == "script":
105
+ return self._call_script_primitive(args)
106
+ if tool["source"] == "hidden_handler":
107
+ return HiddenHandlerBackend(self.repo_root).call_tool(tool, args)
108
+ if tool["source"] == "plugin":
109
+ backend = self._plugin_backend(tool)
110
+ result = backend.call_tool(tool_id, args, self._plugin_context())
111
+ return HostActionExecutor(self, Path.cwd()).complete(backend, tool_id, result)
112
+ if tool["source"] not in BACKENDS:
113
+ raise CliError(f"Tool is handled by a CLI command: {tool_id}", code="CLI_PRIMITIVE_ROUTE")
114
+ backend = self._backend(tool["source"])
115
+ return backend.call_tool(tool["name"], args)
116
+
117
+ def _backend(self, source: str) -> McpBackend:
118
+ try:
119
+ entry = BACKENDS[source]
120
+ except KeyError as exc:
121
+ raise CliError(f"Unsupported backend source: {source}", code="BACKEND_NOT_SUPPORTED") from exc
122
+ return McpBackend(repo_root=self.repo_root, entry=entry, timeout_seconds=self.backend_timeout_seconds or 30)
123
+
124
+ def _plugin_backend(self, tool: dict[str, Any]) -> PluginBackend:
125
+ plugin_id = tool.get("plugin")
126
+ if not isinstance(plugin_id, str) or not plugin_id:
127
+ raise CliError("Plugin tool is missing plugin id", code="PLUGIN_RECORD_NOT_FOUND", details={"tool_id": tool.get("id")})
128
+ return PluginBackend(self.catalog.plugin_record(plugin_id))
129
+
130
+ @staticmethod
131
+ def _plugin_context() -> dict[str, Any]:
132
+ cwd = Path.cwd()
133
+ return {
134
+ "cwd": str(cwd),
135
+ "session_path": str(cwd / ".indesign-cli" / "session.json"),
136
+ "host_tools": sorted(ALLOWED_HOST_ACTIONS),
137
+ }
138
+
139
+ def _call_cli_primitive(self, tool_id: str, args: dict[str, Any]) -> dict[str, Any]:
140
+ if tool_id == "export.verify":
141
+ from .artifacts import parse_timestamp, verify_artifact
142
+
143
+ path = self._require_arg(args, "path")
144
+ try:
145
+ created_after = parse_timestamp(args["created_after"]) if args.get("created_after") else None
146
+ except ValueError as exc:
147
+ raise CliError("created_after must be an ISO timestamp", code="BAD_TIMESTAMP") from exc
148
+ return verify_artifact(Path(path), created_after=created_after, cwd=Path.cwd())
149
+ if tool_id == "session.show":
150
+ from .session import SessionStore
151
+
152
+ return SessionStore(Path.cwd()).read(compact=not bool(args.get("verbose")))
153
+ if tool_id == "session.clear":
154
+ from .session import SessionStore
155
+
156
+ SessionStore(Path.cwd()).clear()
157
+ return {"cleared": True}
158
+ if tool_id == "server.health":
159
+ from .health import health
160
+
161
+ return health(self.repo_root, deep=bool(args.get("deep")))
162
+ if tool_id == "server.setup":
163
+ from .node_setup import setup_node_dependencies
164
+
165
+ return setup_node_dependencies(self.repo_root)
166
+ if tool_id == "skill.install":
167
+ from .runtime import install_skill
168
+
169
+ return install_skill(Path(args.get("target") or "."))
170
+ raise CliError(f"Unsupported CLI primitive: {tool_id}", code="CLI_PRIMITIVE_UNSUPPORTED")
171
+
172
+ def _call_script_primitive(self, args: dict[str, Any]) -> dict[str, Any]:
173
+ from .scripts import run_script, run_stdin_script
174
+
175
+ old_timeout = self.backend_timeout_seconds
176
+ if args.get("timeout") is not None:
177
+ self.backend_timeout_seconds = self._parse_timeout(args.get("timeout"))
178
+ try:
179
+ if args.get("stdin"):
180
+ return run_stdin_script(self, Path.cwd())
181
+ if args.get("file"):
182
+ return run_script(self, Path(args["file"]))
183
+ raise CliError("script.run requires file or stdin", code="SCRIPT_INPUT_REQUIRED")
184
+ finally:
185
+ self.backend_timeout_seconds = old_timeout
186
+
187
+ @staticmethod
188
+ def _require_arg(args: dict[str, Any], key: str) -> Any:
189
+ value = args.get(key)
190
+ if value in (None, ""):
191
+ raise CliError(f"Missing required argument: {key}", code="MISSING_ARGUMENT", details={"argument": key})
192
+ return value
193
+
194
+ @staticmethod
195
+ def _parse_timeout(value: Any) -> int:
196
+ try:
197
+ timeout = int(value)
198
+ except (TypeError, ValueError) as exc:
199
+ raise CliError("timeout must be an integer number of seconds", code="BAD_TIMEOUT") from exc
200
+ if timeout < 1 or timeout > 3600:
201
+ raise CliError("timeout must be between 1 and 3600 seconds", code="BAD_TIMEOUT", details={"timeout": timeout})
202
+ return timeout
203
+
204
+
205
+ def load_args(path_value: str) -> dict[str, Any]:
206
+ try:
207
+ if path_value == "-":
208
+ payload = json.loads(sys.stdin.read() or "{}")
209
+ else:
210
+ payload = json.loads(Path(path_value).read_text(encoding="utf-8-sig"))
211
+ except FileNotFoundError as exc:
212
+ raise CliError("Arguments file not found", code="ARGS_FILE_NOT_FOUND") from exc
213
+ except JSONDecodeError as exc:
214
+ raise CliError("Arguments must be valid JSON", code="ARGS_JSON_INVALID") from exc
215
+ if not isinstance(payload, dict):
216
+ raise CliError("Arguments JSON must be an object", code="ARGS_NOT_OBJECT")
217
+ return payload
@@ -0,0 +1,59 @@
1
+ from __future__ import annotations
2
+
3
+ import os
4
+ import shutil
5
+ from pathlib import Path
6
+
7
+ from .errors import CliError
8
+
9
+
10
+ def package_root() -> Path:
11
+ return Path(__file__).resolve().parents[1]
12
+
13
+
14
+ def _is_server_root(path: Path) -> bool:
15
+ return (
16
+ (path / "package.json").exists()
17
+ and (path / "src" / "index.js").exists()
18
+ and (path / "src" / "advanced" / "index.js").exists()
19
+ )
20
+
21
+
22
+ def resolve_server_root() -> Path:
23
+ package_dir = package_root()
24
+ override = os.environ.get("INDESIGN_CLI_SERVER_ROOT")
25
+ if override:
26
+ path = Path(override).resolve()
27
+ if _is_server_root(path):
28
+ return path
29
+ raise CliError("INDESIGN_CLI_SERVER_ROOT does not point to a valid server root", code="SERVER_ROOT_INVALID")
30
+ for candidate in [*package_dir.parents, package_dir / "server"]:
31
+ if _is_server_root(candidate):
32
+ return candidate
33
+ raise CliError("InDesign CLI server resources were not found", code="SERVER_ROOT_NOT_FOUND")
34
+
35
+
36
+ def hidden_handler_bridge_path() -> Path:
37
+ bridge = package_root() / "node" / "hidden_handler_bridge.mjs"
38
+ if not bridge.exists():
39
+ raise CliError("Hidden handler bridge not found", code="HIDDEN_HANDLER_BRIDGE_NOT_FOUND")
40
+ return bridge
41
+
42
+
43
+ def skill_source_path() -> Path:
44
+ path = package_root() / "skills" / "SKILL.md"
45
+ if not path.exists():
46
+ raise CliError("Packaged skill not found", code="SKILL_NOT_FOUND")
47
+ return path
48
+
49
+
50
+ def install_skill(target: Path) -> dict[str, str]:
51
+ target_root = target.resolve()
52
+ destination = target_root / ".codex" / "skills" / "indesign-cli" / "SKILL.md"
53
+ destination.parent.mkdir(parents=True, exist_ok=True)
54
+ shutil.copy2(skill_source_path(), destination)
55
+ return {
56
+ "target": str(target_root),
57
+ "installed_path": str(destination),
58
+ "skill": "indesign-cli",
59
+ }
@@ -0,0 +1,44 @@
1
+ from __future__ import annotations
2
+
3
+ import locale
4
+ import sys
5
+ from pathlib import Path
6
+ from typing import Any
7
+
8
+
9
+ def _read_stdin_text() -> str:
10
+ buffer = getattr(sys.stdin, "buffer", None)
11
+ if buffer is None:
12
+ return sys.stdin.read()
13
+
14
+ raw = buffer.read()
15
+ if isinstance(raw, str):
16
+ return raw
17
+
18
+ encodings = ["utf-8-sig", "utf-8", locale.getpreferredencoding(False)]
19
+ if sys.platform.startswith("win"):
20
+ encodings.append("mbcs")
21
+
22
+ tried: set[str] = set()
23
+ for encoding in encodings:
24
+ if not encoding or encoding in tried:
25
+ continue
26
+ tried.add(encoding)
27
+ try:
28
+ return raw.decode(encoding)
29
+ except UnicodeDecodeError:
30
+ continue
31
+ return raw.decode("utf-8", errors="replace")
32
+
33
+
34
+ def run_script(router: Any, script_path: Path) -> dict[str, Any]:
35
+ resolved = script_path.resolve()
36
+ return router.call("template.run_jsx_file", {"filePath": str(resolved)})
37
+
38
+
39
+ def run_stdin_script(router: Any, cwd: Path) -> dict[str, Any]:
40
+ tmp_dir = cwd / ".indesign-cli" / "tmp"
41
+ tmp_dir.mkdir(parents=True, exist_ok=True)
42
+ script_path = tmp_dir / "stdin.jsx"
43
+ script_path.write_text(_read_stdin_text(), encoding="utf-8")
44
+ return run_script(router, script_path)
@@ -0,0 +1,68 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ from datetime import datetime, timezone
5
+ from pathlib import Path
6
+ from typing import Any
7
+
8
+
9
+ class SessionStore:
10
+ def __init__(self, cwd: Path) -> None:
11
+ self.root = cwd / ".indesign-cli"
12
+ self.path = self.root / "session.json"
13
+
14
+ def read(self, compact: bool = True) -> dict[str, Any]:
15
+ if not self.path.exists():
16
+ return {"version": 1, "recent_calls": []}
17
+ payload = json.loads(self.path.read_text(encoding="utf-8"))
18
+ if compact:
19
+ payload.pop("verbose_paths", None)
20
+ return payload
21
+
22
+ def write(self, payload: dict[str, Any]) -> None:
23
+ self.root.mkdir(parents=True, exist_ok=True)
24
+ self.path.write_text(json.dumps(payload, ensure_ascii=False, indent=2), encoding="utf-8")
25
+
26
+ def record_call(
27
+ self,
28
+ *,
29
+ tool_id: str,
30
+ domain: str,
31
+ source: str,
32
+ ok: bool,
33
+ duration_ms: int,
34
+ plugin: str | None = None,
35
+ artifacts: list[dict[str, Any]] | None = None,
36
+ ) -> None:
37
+ payload = self.read(compact=False)
38
+ calls = payload.setdefault("recent_calls", [])
39
+ item: dict[str, Any] = {
40
+ "tool_id": tool_id,
41
+ "domain": domain,
42
+ "source": source,
43
+ "ok": ok,
44
+ "duration_ms": duration_ms,
45
+ "time": datetime.now(timezone.utc).isoformat(),
46
+ }
47
+ if plugin:
48
+ item["plugin"] = plugin
49
+ if artifacts:
50
+ item["artifacts"] = [
51
+ {
52
+ "kind": artifact.get("kind"),
53
+ "path": artifact.get("path"),
54
+ }
55
+ for artifact in artifacts
56
+ if isinstance(artifact, dict) and artifact.get("path")
57
+ ][:10]
58
+ calls.insert(
59
+ 0,
60
+ item,
61
+ )
62
+ payload["version"] = 1
63
+ payload["recent_calls"] = calls[:20]
64
+ self.write(payload)
65
+
66
+ def clear(self) -> None:
67
+ if self.path.exists():
68
+ self.path.unlink()