plugsync-cli 0.1.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.
- plugsync_cli/__init__.py +2 -0
- plugsync_cli/bundler.py +77 -0
- plugsync_cli/client.py +201 -0
- plugsync_cli/commands/__init__.py +1 -0
- plugsync_cli/commands/auth.py +156 -0
- plugsync_cli/commands/diff.py +169 -0
- plugsync_cli/commands/log.py +104 -0
- plugsync_cli/commands/plugin.py +470 -0
- plugsync_cli/commands/preview.py +107 -0
- plugsync_cli/commands/pull.py +121 -0
- plugsync_cli/commands/push.py +101 -0
- plugsync_cli/commands/rollback.py +72 -0
- plugsync_cli/commands/validate.py +66 -0
- plugsync_cli/compat.py +73 -0
- plugsync_cli/config.py +51 -0
- plugsync_cli/main.py +84 -0
- plugsync_cli/serializer.py +205 -0
- plugsync_cli-0.1.0.dist-info/METADATA +93 -0
- plugsync_cli-0.1.0.dist-info/RECORD +22 -0
- plugsync_cli-0.1.0.dist-info/WHEEL +5 -0
- plugsync_cli-0.1.0.dist-info/entry_points.txt +2 -0
- plugsync_cli-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
"""plugsync pull — download connector as local directory."""
|
|
2
|
+
import io
|
|
3
|
+
import zipfile
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
import click
|
|
7
|
+
import yaml
|
|
8
|
+
from rich.console import Console
|
|
9
|
+
|
|
10
|
+
from plugsync_cli.client import PlugSyncClient
|
|
11
|
+
from plugsync_cli.serializer import write_directory
|
|
12
|
+
|
|
13
|
+
console = Console()
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@click.command()
|
|
17
|
+
@click.argument("connector_name")
|
|
18
|
+
@click.option("--revision", "-r", type=int, default=None, help="Export a specific revision version")
|
|
19
|
+
@click.option("--output", "-o", type=click.Path(), default=None, help="Output directory path")
|
|
20
|
+
def pull(connector_name: str, revision: int | None, output: str | None):
|
|
21
|
+
"""Download a connector as a local directory.
|
|
22
|
+
|
|
23
|
+
Examples:
|
|
24
|
+
plugsync pull hubspot-juve
|
|
25
|
+
plugsync pull hubspot-juve --revision 3
|
|
26
|
+
plugsync pull hubspot-juve -o ./my-dir
|
|
27
|
+
"""
|
|
28
|
+
try:
|
|
29
|
+
client = PlugSyncClient()
|
|
30
|
+
except RuntimeError as e:
|
|
31
|
+
console.print(f"[red]{e}[/red]")
|
|
32
|
+
raise SystemExit(1)
|
|
33
|
+
|
|
34
|
+
# Find connector by name
|
|
35
|
+
connector = client.find_connector(connector_name)
|
|
36
|
+
if not connector:
|
|
37
|
+
console.print(f"[red]Connector '{connector_name}' not found[/red]")
|
|
38
|
+
raise SystemExit(1)
|
|
39
|
+
|
|
40
|
+
connector_id = connector["id"]
|
|
41
|
+
|
|
42
|
+
# Download ZIP
|
|
43
|
+
if revision is not None:
|
|
44
|
+
console.print(f"Exporting revision v{revision} of [bold]{connector_name}[/bold]...")
|
|
45
|
+
zip_data = client.export_revision(connector_id, revision)
|
|
46
|
+
else:
|
|
47
|
+
console.print(f"Exporting [bold]{connector_name}[/bold]...")
|
|
48
|
+
zip_data = client.export_working_state(connector_id)
|
|
49
|
+
|
|
50
|
+
# Extract ZIP to local directory
|
|
51
|
+
output_dir = Path(output) if output else Path.cwd() / connector_name
|
|
52
|
+
|
|
53
|
+
buf = io.BytesIO(zip_data)
|
|
54
|
+
with zipfile.ZipFile(buf, "r") as zf:
|
|
55
|
+
# Find the prefix directory in the ZIP
|
|
56
|
+
names = zf.namelist()
|
|
57
|
+
if not names:
|
|
58
|
+
console.print("[red]Empty export[/red]")
|
|
59
|
+
raise SystemExit(1)
|
|
60
|
+
|
|
61
|
+
prefix = names[0].split("/")[0]
|
|
62
|
+
|
|
63
|
+
# Parse manifest
|
|
64
|
+
manifest_content = zf.read(f"{prefix}/plugsync.yaml").decode("utf-8")
|
|
65
|
+
manifest = yaml.safe_load(manifest_content)
|
|
66
|
+
|
|
67
|
+
# Parse entities
|
|
68
|
+
entities = []
|
|
69
|
+
for name in sorted(names):
|
|
70
|
+
if name.startswith(f"{prefix}/entities/") and name.endswith(".yaml"):
|
|
71
|
+
data = yaml.safe_load(zf.read(name).decode("utf-8"))
|
|
72
|
+
entity_name = data.get("name", Path(name).stem)
|
|
73
|
+
config = {}
|
|
74
|
+
for key in ("entity_type", "hubspot_object_type", "identity", "field_mappings", "associations", "scope_overrides"):
|
|
75
|
+
if key in data:
|
|
76
|
+
config[key] = data[key]
|
|
77
|
+
entities.append({"name": entity_name, "config": config})
|
|
78
|
+
|
|
79
|
+
# Parse handlers
|
|
80
|
+
handlers = []
|
|
81
|
+
for name in sorted(names):
|
|
82
|
+
if name.startswith(f"{prefix}/handlers/") and name.endswith(".py"):
|
|
83
|
+
content = zf.read(name).decode("utf-8")
|
|
84
|
+
lines = content.split("\n")
|
|
85
|
+
event_type = None
|
|
86
|
+
code = content
|
|
87
|
+
if lines[0].startswith("# event_type:"):
|
|
88
|
+
event_type = lines[0].split(":", 1)[1].strip()
|
|
89
|
+
code = "\n".join(lines[1:]).lstrip("\n")
|
|
90
|
+
|
|
91
|
+
if not event_type:
|
|
92
|
+
stem = Path(name).stem
|
|
93
|
+
event_type = stem.replace("_", ".")
|
|
94
|
+
|
|
95
|
+
# Extract function name
|
|
96
|
+
fn_name = "handler"
|
|
97
|
+
for line in code.split("\n"):
|
|
98
|
+
s = line.strip()
|
|
99
|
+
if s.startswith("async def "):
|
|
100
|
+
fn_name = s.split("(")[0].replace("async def ", "")
|
|
101
|
+
break
|
|
102
|
+
elif s.startswith("def "):
|
|
103
|
+
fn_name = s.split("(")[0].replace("def ", "")
|
|
104
|
+
break
|
|
105
|
+
|
|
106
|
+
handlers.append({"event_type": event_type, "function_name": fn_name, "code": code})
|
|
107
|
+
|
|
108
|
+
# Write to local directory
|
|
109
|
+
write_directory(output_dir, manifest, entities, handlers)
|
|
110
|
+
|
|
111
|
+
# Write .plugsync.yaml in the directory for context
|
|
112
|
+
local_config = output_dir / ".plugsync.yaml"
|
|
113
|
+
local_config.write_text(yaml.dump({
|
|
114
|
+
"connector": connector_name,
|
|
115
|
+
"connector_id": connector_id,
|
|
116
|
+
}, default_flow_style=False))
|
|
117
|
+
|
|
118
|
+
entity_count = len(entities)
|
|
119
|
+
handler_count = len(handlers)
|
|
120
|
+
console.print(f"[green]Pulled to {output_dir}/[/green]")
|
|
121
|
+
console.print(f" {entity_count} entit{'y' if entity_count == 1 else 'ies'}, {handler_count} handler{'s' if handler_count != 1 else ''}")
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
"""plugsync push — upload local directory, validate, and publish."""
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
import click
|
|
5
|
+
import yaml
|
|
6
|
+
from rich.console import Console
|
|
7
|
+
|
|
8
|
+
from plugsync_cli.client import PlugSyncClient
|
|
9
|
+
from plugsync_cli.serializer import read_directory_to_zip
|
|
10
|
+
|
|
11
|
+
console = Console()
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _find_connector_dir(path: str | None) -> Path:
|
|
15
|
+
"""Resolve connector directory from argument or cwd."""
|
|
16
|
+
if path:
|
|
17
|
+
p = Path(path)
|
|
18
|
+
if not p.exists():
|
|
19
|
+
console.print(f"[red]Directory not found: {path}[/red]")
|
|
20
|
+
raise SystemExit(1)
|
|
21
|
+
return p
|
|
22
|
+
|
|
23
|
+
# Check cwd for plugsync.yaml
|
|
24
|
+
cwd = Path.cwd()
|
|
25
|
+
if (cwd / "plugsync.yaml").exists():
|
|
26
|
+
return cwd
|
|
27
|
+
|
|
28
|
+
console.print("[red]No plugsync.yaml found in current directory. Specify a path or cd into a connector directory.[/red]")
|
|
29
|
+
raise SystemExit(1)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _get_connector_id(connector_dir: Path) -> str:
|
|
33
|
+
"""Read connector_id from .plugsync.yaml."""
|
|
34
|
+
local_config = connector_dir / ".plugsync.yaml"
|
|
35
|
+
if local_config.exists():
|
|
36
|
+
with open(local_config) as f:
|
|
37
|
+
data = yaml.safe_load(f) or {}
|
|
38
|
+
cid = data.get("connector_id")
|
|
39
|
+
if cid:
|
|
40
|
+
return cid
|
|
41
|
+
|
|
42
|
+
console.print("[red]No .plugsync.yaml with connector_id found. Run 'plugsync pull' first.[/red]")
|
|
43
|
+
raise SystemExit(1)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@click.command()
|
|
47
|
+
@click.argument("path", required=False, default=None)
|
|
48
|
+
@click.option("--message", "-m", type=str, default=None, help="Change summary for publish")
|
|
49
|
+
@click.option("--no-publish", is_flag=True, default=False, help="Import without publishing")
|
|
50
|
+
def push(path: str | None, message: str | None, no_publish: bool):
|
|
51
|
+
"""Upload local directory to PluSync, validate, and publish.
|
|
52
|
+
|
|
53
|
+
Examples:
|
|
54
|
+
plugsync push # push from cwd
|
|
55
|
+
plugsync push ./hubspot-juve # push from specific path
|
|
56
|
+
plugsync push -m "Added phone" # with change summary
|
|
57
|
+
plugsync push --no-publish # import without publishing
|
|
58
|
+
"""
|
|
59
|
+
try:
|
|
60
|
+
client = PlugSyncClient()
|
|
61
|
+
except RuntimeError as e:
|
|
62
|
+
console.print(f"[red]{e}[/red]")
|
|
63
|
+
raise SystemExit(1)
|
|
64
|
+
|
|
65
|
+
connector_dir = _find_connector_dir(path)
|
|
66
|
+
connector_id = _get_connector_id(connector_dir)
|
|
67
|
+
|
|
68
|
+
console.print(f"Packing [bold]{connector_dir.name}[/bold]...")
|
|
69
|
+
zip_data = read_directory_to_zip(connector_dir)
|
|
70
|
+
|
|
71
|
+
# Upload
|
|
72
|
+
console.print("Uploading...")
|
|
73
|
+
import_result = client.import_zip(connector_id, zip_data)
|
|
74
|
+
console.print(
|
|
75
|
+
f" Imported: {import_result['entities_count']} entities, "
|
|
76
|
+
f"{import_result['handlers_count']} handlers"
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
# Validate
|
|
80
|
+
console.print("Validating...")
|
|
81
|
+
validation = client.validate(connector_id)
|
|
82
|
+
if not validation["valid"]:
|
|
83
|
+
console.print("[red]Validation failed:[/red]")
|
|
84
|
+
for err in validation.get("errors", []):
|
|
85
|
+
loc = err.get("location", "")
|
|
86
|
+
msg = err.get("message", "")
|
|
87
|
+
console.print(f" [red]{loc}: {msg}[/red]")
|
|
88
|
+
console.print("[yellow]Changes imported but NOT published. Fix errors and push again.[/yellow]")
|
|
89
|
+
raise SystemExit(1)
|
|
90
|
+
|
|
91
|
+
console.print("[green]Validation passed[/green]")
|
|
92
|
+
|
|
93
|
+
if no_publish:
|
|
94
|
+
console.print("[yellow]Skipping publish (--no-publish). Changes are in working state.[/yellow]")
|
|
95
|
+
return
|
|
96
|
+
|
|
97
|
+
# Publish
|
|
98
|
+
console.print("Publishing...")
|
|
99
|
+
pub_result = client.publish(connector_id, change_summary=message)
|
|
100
|
+
version = pub_result.get("version", "?")
|
|
101
|
+
console.print(f"[green]Published revision v{version}[/green]")
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
"""plugsync rollback — restore working state from a past revision."""
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
import click
|
|
5
|
+
import yaml
|
|
6
|
+
from rich.console import Console
|
|
7
|
+
|
|
8
|
+
from plugsync_cli.client import PlugSyncClient
|
|
9
|
+
|
|
10
|
+
console = Console()
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _parse_version(version_str: str) -> int:
|
|
14
|
+
"""Parse version from 'v3' or '3'."""
|
|
15
|
+
cleaned = version_str.strip().lower().lstrip("v")
|
|
16
|
+
try:
|
|
17
|
+
return int(cleaned)
|
|
18
|
+
except ValueError:
|
|
19
|
+
console.print(f"[red]Invalid version: '{version_str}'. Use 'v3' or '3'.[/red]")
|
|
20
|
+
raise SystemExit(1)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@click.command()
|
|
24
|
+
@click.argument("version")
|
|
25
|
+
@click.argument("connector_name", required=False, default=None)
|
|
26
|
+
def rollback(version: str, connector_name: str | None):
|
|
27
|
+
"""Restore working state from a past revision.
|
|
28
|
+
|
|
29
|
+
This overwrites the current working state with the specified revision.
|
|
30
|
+
Does NOT auto-publish. Run 'plugsync push' to publish the restored state.
|
|
31
|
+
|
|
32
|
+
Examples:
|
|
33
|
+
plugsync rollback v3
|
|
34
|
+
plugsync rollback 3
|
|
35
|
+
plugsync rollback v3 hubspot-juve
|
|
36
|
+
"""
|
|
37
|
+
try:
|
|
38
|
+
client = PlugSyncClient()
|
|
39
|
+
except RuntimeError as e:
|
|
40
|
+
console.print(f"[red]{e}[/red]")
|
|
41
|
+
raise SystemExit(1)
|
|
42
|
+
|
|
43
|
+
ver = _parse_version(version)
|
|
44
|
+
|
|
45
|
+
# Resolve connector_id
|
|
46
|
+
connector_id = None
|
|
47
|
+
if connector_name:
|
|
48
|
+
connector = client.find_connector(connector_name)
|
|
49
|
+
if not connector:
|
|
50
|
+
console.print(f"[red]Connector '{connector_name}' not found[/red]")
|
|
51
|
+
raise SystemExit(1)
|
|
52
|
+
connector_id = connector["id"]
|
|
53
|
+
else:
|
|
54
|
+
local_config = Path.cwd() / ".plugsync.yaml"
|
|
55
|
+
if local_config.exists():
|
|
56
|
+
with open(local_config) as f:
|
|
57
|
+
data = yaml.safe_load(f) or {}
|
|
58
|
+
connector_id = data.get("connector_id")
|
|
59
|
+
|
|
60
|
+
if not connector_id:
|
|
61
|
+
console.print("[red]Specify a connector name or run from a connector directory.[/red]")
|
|
62
|
+
raise SystemExit(1)
|
|
63
|
+
|
|
64
|
+
console.print(f"Restoring to v{ver}...")
|
|
65
|
+
result = client.restore_revision(connector_id, ver)
|
|
66
|
+
|
|
67
|
+
if result.get("restored"):
|
|
68
|
+
console.print(f"[green]Working state restored to v{ver}[/green]")
|
|
69
|
+
console.print(f"Run [bold]plugsync push[/bold] to publish, or [bold]plugsync pull[/bold] to update local files.")
|
|
70
|
+
else:
|
|
71
|
+
console.print(f"[red]Failed to restore v{ver}[/red]")
|
|
72
|
+
raise SystemExit(1)
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"""plugsync validate — validate current connector state."""
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
import click
|
|
5
|
+
import yaml
|
|
6
|
+
from rich.console import Console
|
|
7
|
+
|
|
8
|
+
from plugsync_cli.client import PlugSyncClient
|
|
9
|
+
from plugsync_cli.serializer import read_directory_to_zip
|
|
10
|
+
|
|
11
|
+
console = Console()
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@click.command()
|
|
15
|
+
@click.argument("path", required=False, default=None)
|
|
16
|
+
def validate(path: str | None):
|
|
17
|
+
"""Validate the connector configuration.
|
|
18
|
+
|
|
19
|
+
Uploads the local state to the server and runs validation
|
|
20
|
+
(schema, syntax, referential integrity).
|
|
21
|
+
|
|
22
|
+
Examples:
|
|
23
|
+
plugsync validate
|
|
24
|
+
plugsync validate ./hubspot-juve
|
|
25
|
+
"""
|
|
26
|
+
try:
|
|
27
|
+
client = PlugSyncClient()
|
|
28
|
+
except RuntimeError as e:
|
|
29
|
+
console.print(f"[red]{e}[/red]")
|
|
30
|
+
raise SystemExit(1)
|
|
31
|
+
|
|
32
|
+
connector_dir = Path(path) if path else Path.cwd()
|
|
33
|
+
if not (connector_dir / "plugsync.yaml").exists():
|
|
34
|
+
console.print("[red]No plugsync.yaml found.[/red]")
|
|
35
|
+
raise SystemExit(1)
|
|
36
|
+
|
|
37
|
+
local_config = connector_dir / ".plugsync.yaml"
|
|
38
|
+
if not local_config.exists():
|
|
39
|
+
console.print("[red]No .plugsync.yaml found. Run 'plugsync pull' first.[/red]")
|
|
40
|
+
raise SystemExit(1)
|
|
41
|
+
|
|
42
|
+
with open(local_config) as f:
|
|
43
|
+
data = yaml.safe_load(f) or {}
|
|
44
|
+
connector_id = data.get("connector_id")
|
|
45
|
+
|
|
46
|
+
# Upload current state (required for server-side validation)
|
|
47
|
+
zip_data = read_directory_to_zip(connector_dir)
|
|
48
|
+
click.confirm(
|
|
49
|
+
"This will upload your local files to the remote working state. Continue?",
|
|
50
|
+
abort=True,
|
|
51
|
+
)
|
|
52
|
+
client.import_zip(connector_id, zip_data)
|
|
53
|
+
|
|
54
|
+
# Validate
|
|
55
|
+
result = client.validate(connector_id)
|
|
56
|
+
|
|
57
|
+
if result["valid"]:
|
|
58
|
+
console.print("[green]Validation passed[/green]")
|
|
59
|
+
return
|
|
60
|
+
|
|
61
|
+
console.print("[red]Validation failed:[/red]")
|
|
62
|
+
for err in result.get("errors", []):
|
|
63
|
+
loc = err.get("location", "")
|
|
64
|
+
msg = err.get("message", "")
|
|
65
|
+
console.print(f" [red]{loc}: {msg}[/red]")
|
|
66
|
+
raise SystemExit(1)
|
plugsync_cli/compat.py
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
"""API version compatibility check (issue #957).
|
|
2
|
+
|
|
3
|
+
The CLI ships on its own semver line, independent of the backend release
|
|
4
|
+
train. This module declares the minimum backend API version the current
|
|
5
|
+
CLI build expects, and checks it against the API's version endpoint
|
|
6
|
+
before any command that talks to the API runs.
|
|
7
|
+
|
|
8
|
+
As of this writing the backend does not expose that version endpoint yet
|
|
9
|
+
(tracked in issue #992) -- until it ships, every check degrades
|
|
10
|
+
gracefully to a silent no-op rather than a hard failure: an
|
|
11
|
+
absent/unreachable endpoint is evidence of an API that hasn't been
|
|
12
|
+
instrumented yet, not of one that's incompatible.
|
|
13
|
+
"""
|
|
14
|
+
import httpx
|
|
15
|
+
from rich.console import Console
|
|
16
|
+
|
|
17
|
+
# Minimum backend API version this CLI build expects. Bump when a CLI
|
|
18
|
+
# release starts depending on an API contract older backends don't have.
|
|
19
|
+
MIN_API_VERSION = "1.0.0"
|
|
20
|
+
|
|
21
|
+
console = Console()
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _parse_version(value: str) -> tuple[int, ...] | None:
|
|
25
|
+
"""Parse a dotted numeric version ("1.4.2") into a tuple, or None if unparseable."""
|
|
26
|
+
try:
|
|
27
|
+
return tuple(int(part) for part in value.strip().split("."))
|
|
28
|
+
except (ValueError, AttributeError):
|
|
29
|
+
return None
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def check_api_compat(api_url: str, min_version: str = MIN_API_VERSION) -> None:
|
|
33
|
+
"""Warn if the API at ``api_url`` reports a version older than ``min_version``.
|
|
34
|
+
|
|
35
|
+
Never raises: any failure to determine the API's version (missing
|
|
36
|
+
endpoint, unreachable host, malformed response, unparseable version
|
|
37
|
+
string) is treated as "unknown" and silently skipped, so a backend
|
|
38
|
+
without the version endpoint is no worse off than one with it.
|
|
39
|
+
"""
|
|
40
|
+
try:
|
|
41
|
+
response = httpx.get(f"{api_url}/api/version", timeout=5.0)
|
|
42
|
+
except httpx.RequestError:
|
|
43
|
+
return
|
|
44
|
+
|
|
45
|
+
if response.status_code == 404:
|
|
46
|
+
return
|
|
47
|
+
|
|
48
|
+
try:
|
|
49
|
+
response.raise_for_status()
|
|
50
|
+
api_version = response.json()["api_version"]
|
|
51
|
+
except Exception:
|
|
52
|
+
return
|
|
53
|
+
|
|
54
|
+
current = _parse_version(api_version)
|
|
55
|
+
minimum = _parse_version(min_version)
|
|
56
|
+
if current is None or minimum is None:
|
|
57
|
+
return
|
|
58
|
+
|
|
59
|
+
# Pad to equal length before comparing: tuple comparison in Python
|
|
60
|
+
# compares element-wise and falls back to length when one is a prefix
|
|
61
|
+
# of the other, so (1, 0) < (1, 0, 0) is True even though "1.0" and
|
|
62
|
+
# "1.0.0" are the same version.
|
|
63
|
+
width = max(len(current), len(minimum))
|
|
64
|
+
current_padded = current + (0,) * (width - len(current))
|
|
65
|
+
minimum_padded = minimum + (0,) * (width - len(minimum))
|
|
66
|
+
|
|
67
|
+
if current_padded < minimum_padded:
|
|
68
|
+
console.print(
|
|
69
|
+
f"[yellow]Warning: API at {api_url} reports version {api_version}, "
|
|
70
|
+
f"below the minimum {min_version} this CLI expects. Some commands "
|
|
71
|
+
"may not work correctly. Upgrade the API, or install a matching "
|
|
72
|
+
"plugsync-cli version.[/yellow]"
|
|
73
|
+
)
|
plugsync_cli/config.py
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"""Manage ~/.plugsync/config.yaml for CLI authentication and settings."""
|
|
2
|
+
import os
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
import yaml
|
|
6
|
+
|
|
7
|
+
CONFIG_DIR = Path.home() / ".plugsync"
|
|
8
|
+
CONFIG_FILE = CONFIG_DIR / "config.yaml"
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def get_config() -> dict:
|
|
12
|
+
"""Load config from ~/.plugsync/config.yaml."""
|
|
13
|
+
if not CONFIG_FILE.exists():
|
|
14
|
+
return {}
|
|
15
|
+
with open(CONFIG_FILE) as f:
|
|
16
|
+
return yaml.safe_load(f) or {}
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def save_config(config: dict) -> None:
|
|
20
|
+
"""Save config to ~/.plugsync/config.yaml."""
|
|
21
|
+
CONFIG_DIR.mkdir(parents=True, exist_ok=True)
|
|
22
|
+
with open(CONFIG_FILE, "w") as f:
|
|
23
|
+
yaml.dump(config, f, default_flow_style=False, sort_keys=False)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def get_api_url() -> str:
|
|
27
|
+
"""Get the API base URL from config or environment."""
|
|
28
|
+
env_url = os.environ.get("PLUGSYNC_API_URL")
|
|
29
|
+
if env_url:
|
|
30
|
+
return env_url.rstrip("/")
|
|
31
|
+
config = get_config()
|
|
32
|
+
return config.get("api_url", "http://localhost:8013").rstrip("/")
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def get_api_key() -> str | None:
|
|
36
|
+
"""Get the org API key from config or environment."""
|
|
37
|
+
env_key = os.environ.get("PLUGSYNC_API_KEY")
|
|
38
|
+
if env_key:
|
|
39
|
+
return env_key
|
|
40
|
+
config = get_config()
|
|
41
|
+
return config.get("api_key")
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def get_default_connector() -> str | None:
|
|
45
|
+
"""Get the default connector name from local .plugsync.yaml in cwd."""
|
|
46
|
+
local_config = Path.cwd() / ".plugsync.yaml"
|
|
47
|
+
if local_config.exists():
|
|
48
|
+
with open(local_config) as f:
|
|
49
|
+
data = yaml.safe_load(f) or {}
|
|
50
|
+
return data.get("connector")
|
|
51
|
+
return None
|
plugsync_cli/main.py
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
"""PluSync CLI entry point — git-like connector management."""
|
|
2
|
+
import click
|
|
3
|
+
from rich.console import Console
|
|
4
|
+
|
|
5
|
+
console = Console()
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@click.group()
|
|
9
|
+
@click.version_option(package_name="plugsync-cli")
|
|
10
|
+
def cli():
|
|
11
|
+
"""PluSync CLI — manage connectors like code."""
|
|
12
|
+
pass
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@cli.group()
|
|
16
|
+
def config():
|
|
17
|
+
"""Manage CLI configuration."""
|
|
18
|
+
pass
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@config.command("set")
|
|
22
|
+
@click.argument("key")
|
|
23
|
+
@click.argument("value")
|
|
24
|
+
def config_set(key: str, value: str):
|
|
25
|
+
"""Set a configuration value (api_url, api_key)."""
|
|
26
|
+
from plugsync_cli.config import get_config, save_config
|
|
27
|
+
|
|
28
|
+
cfg = get_config()
|
|
29
|
+
cfg[key] = value
|
|
30
|
+
save_config(cfg)
|
|
31
|
+
console.print(f"[green]Set {key}[/green]")
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@config.command("get")
|
|
35
|
+
@click.argument("key")
|
|
36
|
+
def config_get(key: str):
|
|
37
|
+
"""Get a configuration value."""
|
|
38
|
+
from plugsync_cli.config import get_config
|
|
39
|
+
|
|
40
|
+
cfg = get_config()
|
|
41
|
+
value = cfg.get(key)
|
|
42
|
+
if value:
|
|
43
|
+
console.print(value)
|
|
44
|
+
else:
|
|
45
|
+
console.print(f"[yellow]{key} not set[/yellow]")
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@config.command("show")
|
|
49
|
+
def config_show():
|
|
50
|
+
"""Show all configuration values."""
|
|
51
|
+
from plugsync_cli.config import get_config
|
|
52
|
+
|
|
53
|
+
cfg = get_config()
|
|
54
|
+
if not cfg:
|
|
55
|
+
console.print("[yellow]No configuration set. Run 'plugsync config set api_key <key>'[/yellow]")
|
|
56
|
+
return
|
|
57
|
+
for key, value in cfg.items():
|
|
58
|
+
if key == "api_key":
|
|
59
|
+
# Mask the key
|
|
60
|
+
display = value[:8] + "..." if len(value) > 8 else "***"
|
|
61
|
+
console.print(f" {key}: {display}")
|
|
62
|
+
else:
|
|
63
|
+
console.print(f" {key}: {value}")
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
from plugsync_cli.commands.pull import pull
|
|
67
|
+
from plugsync_cli.commands.push import push
|
|
68
|
+
from plugsync_cli.commands.diff import diff
|
|
69
|
+
from plugsync_cli.commands.validate import validate
|
|
70
|
+
from plugsync_cli.commands.preview import preview
|
|
71
|
+
from plugsync_cli.commands.log import log_cmd
|
|
72
|
+
from plugsync_cli.commands.rollback import rollback
|
|
73
|
+
from plugsync_cli.commands.plugin import plugin
|
|
74
|
+
from plugsync_cli.commands.auth import auth
|
|
75
|
+
|
|
76
|
+
cli.add_command(pull)
|
|
77
|
+
cli.add_command(push)
|
|
78
|
+
cli.add_command(diff)
|
|
79
|
+
cli.add_command(validate)
|
|
80
|
+
cli.add_command(preview)
|
|
81
|
+
cli.add_command(log_cmd)
|
|
82
|
+
cli.add_command(rollback)
|
|
83
|
+
cli.add_command(plugin)
|
|
84
|
+
cli.add_command(auth)
|