Skip to content

Commit 995a543

Browse files
CrispStrobeclaude
andcommitted
feat: --json output mode for whoami / list-path / resolve
Add a --json flag to the read-side commands so other tools can parse structured output instead of scraping the emoji-decorated default text: python cli.py whoami --json python cli.py list-path /Documents --json python cli.py resolve /Documents/foo.pdf --json Output shapes: whoami → {"logged_in": bool, "user": {email, uuid, rootFolderId}|null} list-path → drive_service.list_folder_with_paths verbatim ({current_path, folders[], files[]}) resolve → drive_service.resolve_path verbatim ({type, uuid, path, metadata}) Errors are emitted as {"error": "<msg>"} on stderr with exit code 1, also under --json. The default human-readable output is unchanged when --json is absent. Motivation: CrispSorter (sibling project) wants to drive list/stat operations from Rust without subprocess text-scraping. With --json the Rust side does plain serde_json::from_slice on the CLI output and gets typed structs back. The same pattern would help any other tool that wants to script around internxt-cli (jq pipelines, other languages, CI). Verified live: `python cli.py whoami --json` against a logged-in account returns valid JSON; downstream Rust deserialisation tests in CrispSorter pass. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent bdd5a98 commit 995a543

1 file changed

Lines changed: 70 additions & 17 deletions

File tree

cli.py

Lines changed: 70 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -187,10 +187,20 @@ def login(email: Optional[str], password: Optional[str], tfa: Optional[str], non
187187

188188

189189
@cli.command()
190-
def whoami():
190+
@click.option('--json', 'json_out', is_flag=True,
191+
help='Emit machine-readable JSON instead of decorated text. '
192+
'Suitable for programmatic consumption from other tools.')
193+
def whoami(json_out: bool):
191194
"""Check current login status"""
192195
try:
193196
user_info = auth_service.whoami()
197+
if json_out:
198+
import json as _json
199+
click.echo(_json.dumps({
200+
'logged_in': bool(user_info),
201+
'user': user_info or None,
202+
}))
203+
return
194204
if user_info:
195205
click.echo(f"📧 Logged in as: {user_info['email']}")
196206
click.echo(f"🆔 User ID: {user_info['uuid']}")
@@ -199,6 +209,10 @@ def whoami():
199209
click.echo("❌ Not logged in")
200210
click.echo("💡 Use 'python cli.py login' to log in")
201211
except Exception as e:
212+
if json_out:
213+
import json as _json
214+
click.echo(_json.dumps({'error': str(e)}), err=True)
215+
sys.exit(1)
202216
click.echo(f"❌ Error: {e}", err=True)
203217

204218

@@ -1258,13 +1272,24 @@ def download(file_uuid: str, destination: str, preserve_timestamps: bool, on_con
12581272
@click.argument('path', default='/')
12591273
@click.option('--detailed', '-d', is_flag=True, help='Show detailed information')
12601274
@click.option('--all', '-a', 'show_all', is_flag=True, help='Show all attributes (verbose)')
1261-
def list_path(path: str, detailed: bool, show_all: bool):
1275+
@click.option('--json', 'json_out', is_flag=True,
1276+
help='Emit machine-readable JSON ({current_path, folders[], files[]}) '
1277+
'instead of decorated text. Suitable for programmatic consumption.')
1278+
def list_path(path: str, detailed: bool, show_all: bool, json_out: bool):
12621279
"""List folder contents with paths (much more user-friendly!)"""
12631280
try:
12641281
auth_service.get_auth_details()
1265-
1282+
12661283
content = drive_service.list_folder_with_paths(path)
1267-
1284+
1285+
if json_out:
1286+
import json as _json
1287+
# Emit the raw drive_service output verbatim — it's already a dict
1288+
# of plain attrs (no Click formatting, no emoji). Consumers can
1289+
# rely on the `current_path` / `folders` / `files` keys.
1290+
click.echo(_json.dumps(content, default=str))
1291+
return
1292+
12681293
click.echo(f"\n📁 Listing folder: {path}")
12691294
click.echo()
12701295
click.echo(f"📁 Contents of: {content['current_path']}")
@@ -1387,12 +1412,20 @@ def list_path(path: str, detailed: bool, show_all: bool):
13871412
click.echo(f" Delete by path: python cli.py trash-path \"{example_path}\"")
13881413

13891414
except ValueError as e:
1390-
click.echo(f"❌ Error: {e}", err=True)
1415+
if json_out:
1416+
import json as _json
1417+
click.echo(_json.dumps({'error': str(e)}), err=True)
1418+
else:
1419+
click.echo(f"❌ Error: {e}", err=True)
13911420
sys.exit(1)
13921421
except Exception as e:
1393-
click.echo(f"❌ Unexpected error: {e}", err=True)
1394-
import traceback
1395-
traceback.print_exc()
1422+
if json_out:
1423+
import json as _json
1424+
click.echo(_json.dumps({'error': str(e)}), err=True)
1425+
else:
1426+
click.echo(f"❌ Unexpected error: {e}", err=True)
1427+
import traceback
1428+
traceback.print_exc()
13961429
sys.exit(1)
13971430

13981431
@cli.command('download-path')
@@ -1780,41 +1813,61 @@ def find(path: str, name_pattern: Optional[str], iname_pattern: Optional[str], m
17801813

17811814
@cli.command()
17821815
@click.argument('path')
1783-
def resolve(path: str):
1816+
@click.option('--json', 'json_out', is_flag=True,
1817+
help='Emit machine-readable JSON ({type, uuid, path, metadata}). '
1818+
'Suitable for programmatic consumption.')
1819+
def resolve(path: str, json_out: bool):
17841820
"""Show what a path points to (debugging tool)"""
17851821
try:
17861822
auth_service.get_auth_details()
1787-
1823+
17881824
resolved = drive_service.resolve_path(path)
1789-
1825+
1826+
if json_out:
1827+
import json as _json
1828+
click.echo(_json.dumps(resolved, default=str))
1829+
return
1830+
17901831
click.echo(f"\n🔍 Path resolution for: {path}")
17911832
click.echo("=" * 50)
17921833
click.echo(f"Type: {resolved['type'].upper()}")
17931834
click.echo(f"UUID: {resolved['uuid']}")
17941835
click.echo(f"Resolved path: {resolved['path']}")
1795-
1836+
17961837
if resolved['type'] == 'file':
17971838
metadata = resolved['metadata']
17981839
file_type = metadata.get('type', '')
17991840
size = format_size(metadata.get('size', 0))
18001841
click.echo(f"File type: {file_type}")
18011842
click.echo(f"Size: {size}")
1802-
1843+
18031844
click.echo("\n💡 You can use this path with:")
18041845
if resolved['type'] == 'file':
18051846
click.echo(f" python cli.py download-path \"{resolved['path']}\"")
18061847
click.echo(f" python cli.py trash-path \"{resolved['path']}\"")
18071848
else:
18081849
click.echo(f" python cli.py list-path \"{resolved['path']}\"")
1809-
1850+
18101851
except FileNotFoundError as e:
1811-
click.echo(f"❌ Path not found: {e}", err=True)
1852+
if json_out:
1853+
import json as _json
1854+
click.echo(_json.dumps({'error': 'not_found', 'message': str(e)}), err=True)
1855+
else:
1856+
click.echo(f"❌ Path not found: {e}", err=True)
18121857
sys.exit(1)
18131858
except ValueError as e:
1814-
click.echo(f"❌ Error: {e}", err=True)
1859+
if json_out:
1860+
import json as _json
1861+
click.echo(_json.dumps({'error': str(e)}), err=True)
1862+
else:
1863+
click.echo(f"❌ Error: {e}", err=True)
18151864
sys.exit(1)
18161865
except Exception as e:
1817-
click.echo(f"❌ Unexpected error: {e}", err=True)
1866+
if json_out:
1867+
import json as _json
1868+
click.echo(_json.dumps({'error': str(e)}), err=True)
1869+
else:
1870+
click.echo(f"❌ Unexpected error: {e}", err=True)
18181871
sys.exit(1)
18191872

18201873

0 commit comments

Comments
 (0)