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.
- cli_anything/indesign/README.md +32 -0
- cli_anything/indesign/__init__.py +1 -0
- cli_anything/indesign/__main__.py +5 -0
- cli_anything/indesign/core/artifacts.py +57 -0
- cli_anything/indesign/core/catalog.py +405 -0
- cli_anything/indesign/core/domains.py +178 -0
- cli_anything/indesign/core/envelope.py +65 -0
- cli_anything/indesign/core/errors.py +30 -0
- cli_anything/indesign/core/health.py +46 -0
- cli_anything/indesign/core/hidden_backend.py +116 -0
- cli_anything/indesign/core/hidden_handler_schemas.py +223 -0
- cli_anything/indesign/core/mcp_backend.py +152 -0
- cli_anything/indesign/core/node_setup.py +35 -0
- cli_anything/indesign/core/paths.py +41 -0
- cli_anything/indesign/core/plugins/__init__.py +2 -0
- cli_anything/indesign/core/plugins/backend.py +90 -0
- cli_anything/indesign/core/plugins/discovery.py +69 -0
- cli_anything/indesign/core/plugins/host_actions.py +76 -0
- cli_anything/indesign/core/plugins/install.py +38 -0
- cli_anything/indesign/core/plugins/manifest.py +279 -0
- cli_anything/indesign/core/plugins/validate.py +181 -0
- cli_anything/indesign/core/router.py +217 -0
- cli_anything/indesign/core/runtime.py +59 -0
- cli_anything/indesign/core/scripts.py +44 -0
- cli_anything/indesign/core/session.py +68 -0
- cli_anything/indesign/indesign_cli.py +320 -0
- cli_anything/indesign/node/hidden_handler_bridge.mjs +111 -0
- cli_anything/indesign/server/package-lock.json +168 -0
- cli_anything/indesign/server/package.json +45 -0
- cli_anything/indesign/server/src/advanced/index.js +76 -0
- cli_anything/indesign/server/src/core/InDesignMCPServer.js +273 -0
- cli_anything/indesign/server/src/core/scriptExecutor.js +271 -0
- cli_anything/indesign/server/src/core/sessionManager.js +545 -0
- cli_anything/indesign/server/src/handlers/advancedTemplateHandlers.js +1072 -0
- cli_anything/indesign/server/src/handlers/bookHandlers.js +490 -0
- cli_anything/indesign/server/src/handlers/documentHandlers.js +1472 -0
- cli_anything/indesign/server/src/handlers/exportHandlers.js +208 -0
- cli_anything/indesign/server/src/handlers/graphicsHandlers.js +605 -0
- cli_anything/indesign/server/src/handlers/groupHandlers.js +358 -0
- cli_anything/indesign/server/src/handlers/helpHandlers.js +347 -0
- cli_anything/indesign/server/src/handlers/index.js +77 -0
- cli_anything/indesign/server/src/handlers/layerHandlers.js +75 -0
- cli_anything/indesign/server/src/handlers/masterSpreadHandlers.js +451 -0
- cli_anything/indesign/server/src/handlers/pageHandlers.js +698 -0
- cli_anything/indesign/server/src/handlers/pageItemHandlers.js +704 -0
- cli_anything/indesign/server/src/handlers/presentationHandlers.js +220 -0
- cli_anything/indesign/server/src/handlers/spreadHandlers.js +348 -0
- cli_anything/indesign/server/src/handlers/styleHandlers.js +458 -0
- cli_anything/indesign/server/src/handlers/textHandlers.js +431 -0
- cli_anything/indesign/server/src/handlers/utilityHandlers.js +83 -0
- cli_anything/indesign/server/src/index.js +17 -0
- cli_anything/indesign/server/src/types/index.js +106 -0
- cli_anything/indesign/server/src/types/toolDefinitionsAdvancedTemplates.js +144 -0
- cli_anything/indesign/server/src/types/toolDefinitionsBook.js +224 -0
- cli_anything/indesign/server/src/types/toolDefinitionsContent.js +353 -0
- cli_anything/indesign/server/src/types/toolDefinitionsDocument.js +409 -0
- cli_anything/indesign/server/src/types/toolDefinitionsExport.js +65 -0
- cli_anything/indesign/server/src/types/toolDefinitionsLayer.js +40 -0
- cli_anything/indesign/server/src/types/toolDefinitionsMasterSpread.js +160 -0
- cli_anything/indesign/server/src/types/toolDefinitionsPage.js +271 -0
- cli_anything/indesign/server/src/types/toolDefinitionsPageItemGroup.js +437 -0
- cli_anything/indesign/server/src/types/toolDefinitionsPresentation.js +83 -0
- cli_anything/indesign/server/src/types/toolDefinitionsSpread.js +158 -0
- cli_anything/indesign/server/src/types/toolDefinitionsUtility.js +40 -0
- cli_anything/indesign/server/src/utils/stringUtils.js +107 -0
- cli_anything/indesign/skills/SKILL.md +198 -0
- indesign_cli-0.2.0.dist-info/METADATA +267 -0
- indesign_cli-0.2.0.dist-info/RECORD +72 -0
- indesign_cli-0.2.0.dist-info/WHEEL +5 -0
- indesign_cli-0.2.0.dist-info/entry_points.txt +3 -0
- indesign_cli-0.2.0.dist-info/licenses/LICENSE +21 -0
- 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()
|