devsync 0.5.5__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 (84) hide show
  1. aiconfigkit/__init__.py +0 -0
  2. aiconfigkit/__main__.py +6 -0
  3. aiconfigkit/ai_tools/__init__.py +0 -0
  4. aiconfigkit/ai_tools/base.py +236 -0
  5. aiconfigkit/ai_tools/capability_registry.py +262 -0
  6. aiconfigkit/ai_tools/claude.py +91 -0
  7. aiconfigkit/ai_tools/claude_desktop.py +97 -0
  8. aiconfigkit/ai_tools/cline.py +92 -0
  9. aiconfigkit/ai_tools/copilot.py +92 -0
  10. aiconfigkit/ai_tools/cursor.py +109 -0
  11. aiconfigkit/ai_tools/detector.py +169 -0
  12. aiconfigkit/ai_tools/kiro.py +85 -0
  13. aiconfigkit/ai_tools/mcp_syncer.py +291 -0
  14. aiconfigkit/ai_tools/roo.py +110 -0
  15. aiconfigkit/ai_tools/translator.py +390 -0
  16. aiconfigkit/ai_tools/winsurf.py +102 -0
  17. aiconfigkit/cli/__init__.py +0 -0
  18. aiconfigkit/cli/delete.py +118 -0
  19. aiconfigkit/cli/download.py +274 -0
  20. aiconfigkit/cli/install.py +237 -0
  21. aiconfigkit/cli/install_new.py +937 -0
  22. aiconfigkit/cli/list.py +275 -0
  23. aiconfigkit/cli/main.py +454 -0
  24. aiconfigkit/cli/mcp_configure.py +232 -0
  25. aiconfigkit/cli/mcp_install.py +166 -0
  26. aiconfigkit/cli/mcp_sync.py +165 -0
  27. aiconfigkit/cli/package.py +383 -0
  28. aiconfigkit/cli/package_create.py +323 -0
  29. aiconfigkit/cli/package_install.py +472 -0
  30. aiconfigkit/cli/template.py +19 -0
  31. aiconfigkit/cli/template_backup.py +261 -0
  32. aiconfigkit/cli/template_init.py +499 -0
  33. aiconfigkit/cli/template_install.py +261 -0
  34. aiconfigkit/cli/template_list.py +172 -0
  35. aiconfigkit/cli/template_uninstall.py +146 -0
  36. aiconfigkit/cli/template_update.py +225 -0
  37. aiconfigkit/cli/template_validate.py +234 -0
  38. aiconfigkit/cli/tools.py +47 -0
  39. aiconfigkit/cli/uninstall.py +125 -0
  40. aiconfigkit/cli/update.py +309 -0
  41. aiconfigkit/core/__init__.py +0 -0
  42. aiconfigkit/core/checksum.py +211 -0
  43. aiconfigkit/core/component_detector.py +905 -0
  44. aiconfigkit/core/conflict_resolution.py +329 -0
  45. aiconfigkit/core/git_operations.py +539 -0
  46. aiconfigkit/core/mcp/__init__.py +1 -0
  47. aiconfigkit/core/mcp/credentials.py +279 -0
  48. aiconfigkit/core/mcp/manager.py +308 -0
  49. aiconfigkit/core/mcp/set_manager.py +1 -0
  50. aiconfigkit/core/mcp/validator.py +1 -0
  51. aiconfigkit/core/models.py +1661 -0
  52. aiconfigkit/core/package_creator.py +743 -0
  53. aiconfigkit/core/package_manifest.py +248 -0
  54. aiconfigkit/core/repository.py +298 -0
  55. aiconfigkit/core/secret_detector.py +438 -0
  56. aiconfigkit/core/template_manifest.py +283 -0
  57. aiconfigkit/core/version.py +201 -0
  58. aiconfigkit/storage/__init__.py +0 -0
  59. aiconfigkit/storage/library.py +429 -0
  60. aiconfigkit/storage/mcp_tracker.py +1 -0
  61. aiconfigkit/storage/package_tracker.py +234 -0
  62. aiconfigkit/storage/template_library.py +229 -0
  63. aiconfigkit/storage/template_tracker.py +296 -0
  64. aiconfigkit/storage/tracker.py +416 -0
  65. aiconfigkit/tui/__init__.py +5 -0
  66. aiconfigkit/tui/installer.py +511 -0
  67. aiconfigkit/utils/__init__.py +0 -0
  68. aiconfigkit/utils/atomic_write.py +90 -0
  69. aiconfigkit/utils/backup.py +169 -0
  70. aiconfigkit/utils/dotenv.py +128 -0
  71. aiconfigkit/utils/git_helpers.py +187 -0
  72. aiconfigkit/utils/logging.py +60 -0
  73. aiconfigkit/utils/namespace.py +134 -0
  74. aiconfigkit/utils/paths.py +205 -0
  75. aiconfigkit/utils/project.py +109 -0
  76. aiconfigkit/utils/streaming.py +216 -0
  77. aiconfigkit/utils/ui.py +194 -0
  78. aiconfigkit/utils/validation.py +187 -0
  79. devsync-0.5.5.dist-info/LICENSE +21 -0
  80. devsync-0.5.5.dist-info/METADATA +477 -0
  81. devsync-0.5.5.dist-info/RECORD +84 -0
  82. devsync-0.5.5.dist-info/WHEEL +5 -0
  83. devsync-0.5.5.dist-info/entry_points.txt +2 -0
  84. devsync-0.5.5.dist-info/top_level.txt +1 -0
@@ -0,0 +1,454 @@
1
+ """Main CLI application entry point."""
2
+
3
+ from typing import Optional
4
+
5
+ import typer
6
+
7
+ from aiconfigkit.cli.delete import delete_from_library
8
+ from aiconfigkit.cli.download import download_instructions
9
+ from aiconfigkit.cli.install_new import install_instruction_unified
10
+ from aiconfigkit.cli.list import list_available, list_installed, list_library
11
+ from aiconfigkit.cli.mcp_configure import mcp_configure_command
12
+ from aiconfigkit.cli.mcp_install import mcp_install_command
13
+ from aiconfigkit.cli.mcp_sync import mcp_sync_command
14
+ from aiconfigkit.cli.package import package_app
15
+ from aiconfigkit.cli.template import template_app
16
+ from aiconfigkit.cli.template_backup import (
17
+ backup_cleanup_command,
18
+ backup_list_command,
19
+ backup_restore_command,
20
+ )
21
+ from aiconfigkit.cli.template_init import init_command as template_init_command
22
+ from aiconfigkit.cli.template_install import install_command as template_install_command
23
+ from aiconfigkit.cli.template_list import list_command as template_list_command
24
+ from aiconfigkit.cli.template_uninstall import uninstall_command as template_uninstall_command
25
+ from aiconfigkit.cli.template_update import update_command as template_update_command
26
+ from aiconfigkit.cli.template_validate import validate_command as template_validate_command
27
+ from aiconfigkit.cli.tools import show_tools
28
+ from aiconfigkit.cli.uninstall import uninstall_instruction
29
+ from aiconfigkit.cli.update import update_repository
30
+
31
+ app = typer.Typer(
32
+ name="instructionkit",
33
+ help="CLI tool for managing AI coding tool instructions",
34
+ add_completion=False,
35
+ )
36
+
37
+ # Create list subcommand group
38
+ list_app = typer.Typer(help="List instructions")
39
+ app.add_typer(list_app, name="list")
40
+
41
+ # Create package subcommand group
42
+ app.add_typer(package_app, name="package")
43
+
44
+ # Create template subcommand group
45
+ app.add_typer(template_app, name="template")
46
+
47
+ # Create backup subcommand group under template
48
+ backup_app = typer.Typer(help="Manage template backups")
49
+ template_app.add_typer(backup_app, name="backup")
50
+
51
+ # Create mcp subcommand group
52
+ mcp_app = typer.Typer(help="Manage MCP server configurations")
53
+ app.add_typer(mcp_app, name="mcp")
54
+
55
+ # Register mcp commands
56
+ mcp_app.command(name="install")(mcp_install_command)
57
+ mcp_app.command(name="configure")(mcp_configure_command)
58
+ mcp_app.command(name="sync")(mcp_sync_command)
59
+
60
+ # Register template commands
61
+ template_app.command(name="init")(template_init_command)
62
+ template_app.command(name="install")(template_install_command)
63
+ template_app.command(name="list")(template_list_command)
64
+ template_app.command(name="update")(template_update_command)
65
+ template_app.command(name="uninstall")(template_uninstall_command)
66
+ template_app.command(name="validate")(template_validate_command)
67
+
68
+ # Register backup commands
69
+ backup_app.command(name="list")(backup_list_command)
70
+ backup_app.command(name="cleanup")(backup_cleanup_command)
71
+ backup_app.command(name="restore")(backup_restore_command)
72
+
73
+
74
+ @list_app.callback(invoke_without_command=True)
75
+ def list_callback(ctx: typer.Context) -> None:
76
+ """List instructions (available, installed, or in library)."""
77
+ # If no subcommand was provided, show help
78
+ if ctx.invoked_subcommand is None:
79
+ typer.echo(ctx.get_help())
80
+ raise typer.Exit(0)
81
+
82
+
83
+ @app.command()
84
+ def install(
85
+ names: Optional[list[str]] = typer.Argument(
86
+ None,
87
+ help="Instruction name(s) to install (use source/name for disambiguation). Can specify multiple.",
88
+ ),
89
+ source: Optional[str] = typer.Option(
90
+ None,
91
+ "--from",
92
+ "-f",
93
+ help="Source URL or path for direct install (bypasses library)",
94
+ ),
95
+ tools: Optional[list[str]] = typer.Option(
96
+ None,
97
+ "--tool",
98
+ "-t",
99
+ help="AI tool(s) to install to (cursor, copilot, windsurf, claude). Can specify multiple times.",
100
+ ),
101
+ conflict: str = typer.Option(
102
+ "prompt",
103
+ "--conflict",
104
+ "-c",
105
+ help="Conflict resolution strategy (prompt [default], skip, rename, overwrite)",
106
+ ),
107
+ bundle: bool = typer.Option(
108
+ False,
109
+ "--bundle",
110
+ "-b",
111
+ help="Install as bundle (multiple instructions)",
112
+ ),
113
+ ) -> None:
114
+ """
115
+ Install instructions from your library or directly from a source.
116
+
117
+ LIBRARY WORKFLOW (Recommended):
118
+ # Browse and select instructions with TUI
119
+ inskit install
120
+
121
+ # Install specific instruction from library
122
+ inskit install python-best-practices
123
+
124
+ # Install from specific source (if multiple sources have same name)
125
+ inskit install company/python-best-practices
126
+
127
+ # Install multiple instructions at once
128
+ inskit install python-style testing-guide api-design
129
+
130
+ # Install to specific tools only
131
+ inskit install python-style --tool cursor --tool windsurf
132
+
133
+ DIRECT INSTALL (Bypasses library):
134
+ # Install directly from source URL
135
+ inskit install python-style --from https://github.com/company/instructions
136
+
137
+ # Install bundle directly
138
+ inskit install python-backend --bundle --from https://github.com/company/instructions
139
+
140
+ TIP: Download to your library first for better management:
141
+ inskit download --from https://github.com/company/instructions
142
+ """
143
+ exit_code = install_instruction_unified(
144
+ names=names,
145
+ repo=source, # Keep backend param name for now
146
+ tools=tools,
147
+ conflict_strategy=conflict,
148
+ bundle=bundle,
149
+ )
150
+ raise typer.Exit(code=exit_code)
151
+
152
+
153
+ @app.command()
154
+ def download(
155
+ source: str = typer.Option(
156
+ ...,
157
+ "--from",
158
+ "-f",
159
+ help="Source URL or local directory path",
160
+ ),
161
+ ref: Optional[str] = typer.Option(
162
+ None,
163
+ "--ref",
164
+ "-r",
165
+ help="Git reference (tag, branch, or commit) to download",
166
+ ),
167
+ alias: Optional[str] = typer.Option(
168
+ None,
169
+ "--as",
170
+ "-a",
171
+ help="Friendly alias for this source (auto-generated if not provided)",
172
+ ),
173
+ force: bool = typer.Option(
174
+ False,
175
+ "--force",
176
+ help="Re-download even if already in library",
177
+ ),
178
+ ) -> None:
179
+ """
180
+ Download instructions from a source into your local library.
181
+
182
+ This downloads and caches instructions locally without installing them.
183
+ After downloading, use 'inskit install' to install instructions.
184
+
185
+ Examples:
186
+
187
+ # Download from GitHub (auto-generates alias)
188
+ inskit download --from github.com/company/instructions
189
+
190
+ # Download specific version
191
+ inskit download --from github.com/company/instructions --ref v1.0.0
192
+
193
+ # Download from branch
194
+ inskit download --from github.com/company/instructions --ref main
195
+
196
+ # Download with custom alias
197
+ inskit download --from github.com/company/instructions --as company
198
+
199
+ # Download from local folder
200
+ inskit download --from ./my-instructions --as local
201
+
202
+ # Force re-download
203
+ inskit download --from github.com/company/instructions --force
204
+ """
205
+ exit_code = download_instructions(repo=source, ref=ref, force=force, alias=alias)
206
+ raise typer.Exit(code=exit_code)
207
+
208
+
209
+ @list_app.command("available")
210
+ def list_available_cmd(
211
+ source: str = typer.Option(..., "--from", "-f", help="Source URL or local directory path"),
212
+ tag: Optional[str] = typer.Option(None, "--tag", "-t", help="Filter by tag"),
213
+ bundles_only: bool = typer.Option(False, "--bundles-only", help="Show only bundles"),
214
+ instructions_only: bool = typer.Option(False, "--instructions-only", help="Show only instructions"),
215
+ ) -> None:
216
+ """
217
+ List available instructions from a source (without downloading).
218
+
219
+ Examples:
220
+
221
+ # List from Git repository
222
+ inskit list available --from github.com/company/instructions
223
+
224
+ # List from local folder
225
+ inskit list available --from ./my-instructions
226
+
227
+ # Filter by tag
228
+ inskit list available --from github.com/company/instructions --tag python
229
+
230
+ # Show only bundles
231
+ inskit list available --from github.com/company/instructions --bundles-only
232
+ """
233
+ exit_code = list_available(
234
+ repo=source, # Keep backend param name for now
235
+ tag=tag,
236
+ bundles_only=bundles_only,
237
+ instructions_only=instructions_only,
238
+ )
239
+ raise typer.Exit(code=exit_code)
240
+
241
+
242
+ @list_app.command("installed")
243
+ def list_installed_cmd(
244
+ tool: Optional[str] = typer.Option(
245
+ None,
246
+ "--tool",
247
+ "-t",
248
+ help="Filter by AI tool (cursor, copilot, windsurf, claude)",
249
+ ),
250
+ source: Optional[str] = typer.Option(
251
+ None,
252
+ "--source",
253
+ "-s",
254
+ help="Filter by source alias or name",
255
+ ),
256
+ ) -> None:
257
+ """
258
+ List installed instructions in your AI tools.
259
+
260
+ Examples:
261
+
262
+ # List all installed instructions
263
+ inskit list installed
264
+
265
+ # Filter by AI tool
266
+ inskit list installed --tool cursor
267
+
268
+ # Filter by source
269
+ inskit list installed --source company
270
+ """
271
+ exit_code = list_installed(tool=tool, repo=source) # Keep backend param name for now
272
+ raise typer.Exit(code=exit_code)
273
+
274
+
275
+ @list_app.command("library")
276
+ def list_library_cmd(
277
+ source: Optional[str] = typer.Option(
278
+ None,
279
+ "--source",
280
+ "-s",
281
+ help="Filter by source alias",
282
+ ),
283
+ instructions: bool = typer.Option(
284
+ False,
285
+ "--instructions",
286
+ "-i",
287
+ help="Show individual instructions instead of repositories",
288
+ ),
289
+ ) -> None:
290
+ """
291
+ List sources and instructions in your local library.
292
+
293
+ Examples:
294
+
295
+ # List all sources in library
296
+ inskit list library
297
+
298
+ # Show individual instructions
299
+ inskit list library --instructions
300
+
301
+ # Filter by source
302
+ inskit list library --source company
303
+ """
304
+ exit_code = list_library(repo_filter=source, show_instructions=instructions)
305
+ raise typer.Exit(code=exit_code)
306
+
307
+
308
+ @app.command()
309
+ def update(
310
+ namespace: Optional[str] = typer.Option(
311
+ None,
312
+ "--namespace",
313
+ "-n",
314
+ help="Repository namespace to update",
315
+ ),
316
+ all_repos: bool = typer.Option(
317
+ False,
318
+ "--all",
319
+ "-a",
320
+ help="Update all repositories in library",
321
+ ),
322
+ ) -> None:
323
+ """
324
+ Update downloaded instructions to their latest versions.
325
+
326
+ This re-downloads instructions from their sources,
327
+ ensuring you have the latest versions in your library.
328
+
329
+ Examples:
330
+
331
+ # Update a specific source
332
+ inskit update --namespace github.com_company_instructions
333
+
334
+ # Update all sources
335
+ inskit update --all
336
+
337
+ # List sources to find namespace
338
+ inskit list library
339
+ """
340
+ exit_code = update_repository(namespace=namespace, all_repos=all_repos)
341
+ raise typer.Exit(code=exit_code)
342
+
343
+
344
+ @app.command()
345
+ def delete(
346
+ namespace: str = typer.Argument(
347
+ ...,
348
+ help="Repository namespace to delete from library",
349
+ ),
350
+ force: bool = typer.Option(
351
+ False,
352
+ "--force",
353
+ "-f",
354
+ help="Skip confirmation prompt",
355
+ ),
356
+ ) -> None:
357
+ """
358
+ Delete a source from your local library.
359
+
360
+ This removes the downloaded instructions from your library but does NOT
361
+ uninstall them from your AI tools. To uninstall, use 'inskit uninstall'.
362
+
363
+ Examples:
364
+
365
+ # Delete a source
366
+ inskit delete github.com_company_instructions
367
+
368
+ # Skip confirmation
369
+ inskit delete github.com_company_instructions --force
370
+
371
+ # List sources to find namespace
372
+ inskit list library
373
+ """
374
+ exit_code = delete_from_library(namespace=namespace, force=force)
375
+ raise typer.Exit(code=exit_code)
376
+
377
+
378
+ @app.command()
379
+ def uninstall(
380
+ name: str = typer.Argument(..., help="Instruction name to uninstall"),
381
+ tool: Optional[str] = typer.Option(
382
+ None,
383
+ "--tool",
384
+ "-t",
385
+ help="AI tool to uninstall from (cursor, copilot, windsurf, claude)",
386
+ ),
387
+ force: bool = typer.Option(
388
+ False,
389
+ "--force",
390
+ "-f",
391
+ help="Skip confirmation prompt",
392
+ ),
393
+ ) -> None:
394
+ """
395
+ Uninstall an instruction from your AI tools.
396
+
397
+ Removes instructions from project level only.
398
+
399
+ Examples:
400
+
401
+ # Uninstall from all tools
402
+ inskit uninstall python-best-practices
403
+
404
+ # Uninstall from specific tool
405
+ inskit uninstall python-best-practices --tool cursor
406
+
407
+ # Skip confirmation
408
+ inskit uninstall python-best-practices --force
409
+ """
410
+ exit_code = uninstall_instruction(name=name, tool=tool, force=force)
411
+ raise typer.Exit(code=exit_code)
412
+
413
+
414
+ @app.command()
415
+ def tools() -> None:
416
+ """
417
+ Show detected AI coding tools.
418
+
419
+ Display which AI coding tools are installed on your system
420
+ and where their configuration directories are located.
421
+ """
422
+ exit_code = show_tools()
423
+ raise typer.Exit(code=exit_code)
424
+
425
+
426
+ @app.command()
427
+ def version() -> None:
428
+ """Show Config Sync version."""
429
+ from importlib.metadata import version as get_version
430
+
431
+ try:
432
+ version = get_version("configsync")
433
+ except Exception:
434
+ version = "unknown"
435
+
436
+ typer.echo(f"Config Sync version {version}")
437
+
438
+
439
+ @app.callback(invoke_without_command=True)
440
+ def main(ctx: typer.Context) -> None:
441
+ """
442
+ InstructionKit - Manage AI coding tool instructions from Git repositories.
443
+
444
+ Install instructions for Cursor, GitHub Copilot, Windsurf, and Claude Code
445
+ from any Git repository (public or private).
446
+ """
447
+ # If no command was provided, show help
448
+ if ctx.invoked_subcommand is None:
449
+ typer.echo(ctx.get_help())
450
+ raise typer.Exit(0)
451
+
452
+
453
+ if __name__ == "__main__":
454
+ app()
@@ -0,0 +1,232 @@
1
+ """CLI command for configuring MCP server credentials."""
2
+
3
+ import logging
4
+ from pathlib import Path
5
+
6
+ import typer
7
+ from rich.console import Console
8
+ from rich.table import Table
9
+
10
+ from aiconfigkit.core.mcp.credentials import CredentialManager
11
+ from aiconfigkit.core.mcp.manager import MCPManager
12
+ from aiconfigkit.core.models import InstallationScope
13
+
14
+ logger = logging.getLogger(__name__)
15
+ console = Console()
16
+
17
+
18
+ def mcp_configure_command(
19
+ server_ref: str = typer.Argument(..., help="Server reference in format 'namespace.server' or just 'namespace'"),
20
+ scope: str = typer.Option("project", "--scope", help="Installation scope: project or global"),
21
+ non_interactive: bool = typer.Option(False, "--non-interactive", help="Read credentials from environment"),
22
+ show_current: bool = typer.Option(False, "--show-current", help="Show current credential values (masked)"),
23
+ json_output: bool = typer.Option(False, "--json", help="Output results as JSON"),
24
+ ) -> int:
25
+ """
26
+ Configure credentials for MCP servers.
27
+
28
+ This command helps you securely configure required environment variables for
29
+ MCP servers. Credentials are stored in .instructionkit/.env (gitignored).
30
+
31
+ Examples:
32
+
33
+ # Configure specific server interactively
34
+ inskit mcp configure backend.github
35
+
36
+ # Configure all servers in a namespace
37
+ inskit mcp configure backend
38
+
39
+ # Configure with non-interactive mode (read from env)
40
+ export GITHUB_TOKEN=ghp_xxxxx
41
+ inskit mcp configure backend.github --non-interactive
42
+
43
+ # Show current credentials (masked)
44
+ inskit mcp configure backend.github --show-current
45
+
46
+ # Configure globally (available in all projects)
47
+ inskit mcp configure backend.github --scope global
48
+ """
49
+ try:
50
+ # Parse scope
51
+ try:
52
+ install_scope = InstallationScope(scope)
53
+ except ValueError:
54
+ console.print(f"[red]Error:[/red] Invalid scope '{scope}'. Must be 'project' or 'global'")
55
+ return 1
56
+
57
+ # Parse server reference
58
+ if "." in server_ref:
59
+ namespace, server_name = server_ref.rsplit(".", 1)
60
+ else:
61
+ namespace = server_ref
62
+ server_name = None
63
+
64
+ # Get library root and managers
65
+ library_root = _get_library_root()
66
+ mcp_manager = MCPManager(library_root)
67
+ cred_manager = CredentialManager()
68
+
69
+ # Load template
70
+ template = mcp_manager.load_template(namespace, scope=install_scope)
71
+
72
+ if not template:
73
+ console.print(f"[red]Error:[/red] Template '{namespace}' not found in {install_scope.value} scope")
74
+ console.print(f"\nInstall it first: [cyan]inskit mcp install <source> --as {namespace}[/cyan]")
75
+ return 1
76
+
77
+ # Get servers to configure
78
+ if server_name:
79
+ # Configure specific server
80
+ server = template.get_server_by_name(server_name)
81
+ if not server:
82
+ console.print(f"[red]Error:[/red] Server '{server_name}' not found in template '{namespace}'")
83
+ console.print(f"\nAvailable servers: {', '.join(s.name for s in template.servers)}")
84
+ return 1
85
+ servers = [server]
86
+ else:
87
+ # Configure all servers in namespace
88
+ servers = template.servers
89
+
90
+ if not servers:
91
+ console.print(f"[yellow]Warning:[/yellow] No MCP servers found in template '{namespace}'")
92
+ return 0
93
+
94
+ # Show current credentials if requested
95
+ if show_current:
96
+ return _show_current_credentials(servers, cred_manager, install_scope, json_output)
97
+
98
+ # Configure each server
99
+ configured_count = 0
100
+ skipped_count = 0
101
+
102
+ for server in servers:
103
+ required_vars = server.get_required_env_vars()
104
+
105
+ if not required_vars:
106
+ console.print(f"[dim]Server '{server.name}' requires no credentials[/dim]")
107
+ skipped_count += 1
108
+ continue
109
+
110
+ try:
111
+ cred_manager.configure_server(
112
+ server,
113
+ scope=install_scope,
114
+ non_interactive=non_interactive,
115
+ )
116
+ configured_count += 1
117
+
118
+ except ValueError as e:
119
+ console.print(f"[red]Error configuring '{server.name}':[/red] {e}")
120
+ return 1
121
+
122
+ # Output results
123
+ if json_output:
124
+ import json
125
+
126
+ result = {
127
+ "success": True,
128
+ "namespace": namespace,
129
+ "configured": configured_count,
130
+ "skipped": skipped_count,
131
+ "total": len(servers),
132
+ "scope": install_scope.value,
133
+ }
134
+ console.print(json.dumps(result, indent=2))
135
+ else:
136
+ console.print("\n[green]✓[/green] Credential configuration complete!")
137
+ console.print(f" Configured: {configured_count} server(s)")
138
+ if skipped_count > 0:
139
+ console.print(f" Skipped: {skipped_count} server(s) (no credentials needed)")
140
+
141
+ # Show env file location
142
+ if install_scope == InstallationScope.GLOBAL:
143
+ env_path = Path.home() / ".instructionkit" / "global" / ".env"
144
+ else:
145
+ env_path = Path.cwd() / ".instructionkit" / ".env"
146
+
147
+ console.print(f"\n[dim]Credentials saved to: {env_path}[/dim]")
148
+ console.print("[dim](This file is automatically gitignored)[/dim]")
149
+
150
+ # Show next steps
151
+ console.print("\n[bold]Next step:[/bold]")
152
+ console.print(" Sync to AI tools: [cyan]inskit mcp sync --tool all[/cyan]")
153
+
154
+ return 0
155
+
156
+ except Exception as e:
157
+ logger.exception("Unexpected error during credential configuration")
158
+ if json_output:
159
+ import json
160
+
161
+ console.print(json.dumps({"success": False, "error": str(e)}, indent=2))
162
+ else:
163
+ console.print(f"[red]Unexpected error:[/red] {e}")
164
+ return 1
165
+
166
+
167
+ def _show_current_credentials(
168
+ servers: list,
169
+ cred_manager: CredentialManager,
170
+ scope: InstallationScope,
171
+ json_output: bool,
172
+ ) -> int:
173
+ """Show current credential values for servers."""
174
+ if json_output:
175
+ import json
176
+ from typing import Any
177
+
178
+ result: dict[str, Any] = {
179
+ "scope": scope.value,
180
+ "servers": [],
181
+ }
182
+
183
+ for server in servers:
184
+ credentials = cred_manager.show_current_credentials(server, scope)
185
+ is_valid, missing = cred_manager.validate_credentials(server, scope)
186
+
187
+ result["servers"].append(
188
+ {
189
+ "name": server.get_fully_qualified_name(),
190
+ "credentials": credentials,
191
+ "is_configured": is_valid,
192
+ "missing": missing,
193
+ }
194
+ )
195
+
196
+ console.print(json.dumps(result, indent=2))
197
+ else:
198
+ table = Table(title=f"Current Credentials ({scope.value} scope)")
199
+ table.add_column("Server", style="cyan")
200
+ table.add_column("Variable", style="yellow")
201
+ table.add_column("Value", style="dim")
202
+ table.add_column("Status", style="green")
203
+
204
+ for server in servers:
205
+ credentials = cred_manager.show_current_credentials(server, scope)
206
+ is_valid, missing = cred_manager.validate_credentials(server, scope)
207
+
208
+ for i, (var_name, masked_value) in enumerate(credentials.items()):
209
+ server_name = server.get_fully_qualified_name() if i == 0 else ""
210
+ status = "✓" if var_name not in missing else "⚠ Missing"
211
+ status_style = "green" if var_name not in missing else "yellow"
212
+
213
+ table.add_row(
214
+ server_name,
215
+ var_name,
216
+ masked_value,
217
+ f"[{status_style}]{status}[/{status_style}]",
218
+ )
219
+
220
+ console.print(table)
221
+
222
+ return 0
223
+
224
+
225
+ def _get_library_root() -> Path:
226
+ """
227
+ Get the library root directory.
228
+
229
+ Returns:
230
+ Path to library root (~/.instructionkit/library/)
231
+ """
232
+ return Path.home() / ".instructionkit" / "library"