@jahanxu/trellis 0.5.9 → 0.6.1
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.
- package/README.md +74 -130
- package/dist/cli/index.js +1 -0
- package/dist/cli/index.js.map +1 -1
- package/dist/commands/init.d.ts +1 -0
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +30 -2
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/update.d.ts.map +1 -1
- package/dist/commands/update.js +11 -39
- package/dist/commands/update.js.map +1 -1
- package/dist/configurators/index.d.ts.map +1 -1
- package/dist/configurators/index.js +15 -3
- package/dist/configurators/index.js.map +1 -1
- package/dist/configurators/kilo.d.ts +1 -1
- package/dist/configurators/kilo.d.ts.map +1 -1
- package/dist/configurators/kilo.js +2 -1
- package/dist/configurators/kilo.js.map +1 -1
- package/dist/configurators/qoder.d.ts +8 -0
- package/dist/configurators/qoder.d.ts.map +1 -0
- package/dist/configurators/qoder.js +52 -0
- package/dist/configurators/qoder.js.map +1 -0
- package/dist/configurators/workflow.d.ts.map +1 -1
- package/dist/configurators/workflow.js +3 -1
- package/dist/configurators/workflow.js.map +1 -1
- package/dist/migrations/manifests/0.3.2.json +9 -0
- package/dist/migrations/manifests/0.3.3.json +9 -0
- package/dist/migrations/manifests/0.3.4.json +21 -0
- package/dist/migrations/manifests/0.3.5.json +9 -0
- package/dist/templates/claude/commands/trellis/record-session.md +12 -16
- package/dist/templates/codex/skills/record-session/SKILL.md +13 -17
- package/dist/templates/cursor/commands/trellis-record-session.md +12 -16
- package/dist/templates/extract.d.ts +7 -0
- package/dist/templates/extract.d.ts.map +1 -1
- package/dist/templates/extract.js +13 -0
- package/dist/templates/extract.js.map +1 -1
- package/dist/templates/gemini/commands/trellis/record-session.toml +12 -16
- package/dist/templates/iflow/commands/trellis/record-session.md +12 -16
- package/dist/templates/iflow/hooks/session-start.py +1 -0
- package/dist/templates/kilo/commands/trellis/record-session.md +12 -16
- package/dist/templates/kilo/index.d.ts +3 -3
- package/dist/templates/kilo/index.d.ts.map +1 -1
- package/dist/templates/kilo/index.js +7 -7
- package/dist/templates/kilo/index.js.map +1 -1
- package/dist/templates/kilo/workflows/before-backend-dev.md +13 -0
- package/dist/templates/kilo/workflows/before-frontend-dev.md +13 -0
- package/dist/templates/kilo/workflows/brainstorm.md +474 -0
- package/dist/templates/kilo/workflows/break-loop.md +125 -0
- package/dist/templates/kilo/workflows/check-backend.md +13 -0
- package/dist/templates/kilo/workflows/check-cross-layer.md +153 -0
- package/dist/templates/kilo/workflows/check-frontend.md +13 -0
- package/dist/templates/kilo/workflows/create-command.md +152 -0
- package/dist/templates/kilo/workflows/finish-work.md +129 -0
- package/dist/templates/kilo/workflows/integrate-skill.md +219 -0
- package/dist/templates/kilo/workflows/onboard.md +358 -0
- package/dist/templates/kilo/workflows/parallel.md +194 -0
- package/dist/templates/kilo/workflows/record-session.md +58 -0
- package/dist/templates/kilo/workflows/start.md +321 -0
- package/dist/templates/kilo/workflows/update-spec.md +285 -0
- package/dist/templates/kiro/skills/record-session/SKILL.md +13 -17
- package/dist/templates/opencode/commands/trellis/record-session.md +12 -16
- package/dist/templates/qoder/index.d.ts +18 -0
- package/dist/templates/qoder/index.d.ts.map +1 -0
- package/dist/templates/qoder/index.js +40 -0
- package/dist/templates/qoder/index.js.map +1 -0
- package/dist/templates/qoder/skills/before-backend-dev/SKILL.md +18 -0
- package/dist/templates/qoder/skills/before-frontend-dev/SKILL.md +18 -0
- package/dist/templates/qoder/skills/brainstorm/SKILL.md +479 -0
- package/dist/templates/qoder/skills/break-loop/SKILL.md +130 -0
- package/dist/templates/qoder/skills/check-backend/SKILL.md +18 -0
- package/dist/templates/qoder/skills/check-cross-layer/SKILL.md +158 -0
- package/dist/templates/qoder/skills/check-frontend/SKILL.md +18 -0
- package/dist/templates/qoder/skills/create-command/SKILL.md +101 -0
- package/dist/templates/qoder/skills/finish-work/SKILL.md +134 -0
- package/dist/templates/qoder/skills/integrate-skill/SKILL.md +221 -0
- package/dist/templates/qoder/skills/onboard/SKILL.md +363 -0
- package/dist/templates/qoder/skills/record-session/SKILL.md +63 -0
- package/dist/templates/qoder/skills/start/SKILL.md +326 -0
- package/dist/templates/qoder/skills/update-spec/SKILL.md +290 -0
- package/dist/templates/trellis/config.yaml +15 -0
- package/dist/templates/trellis/index.d.ts +3 -0
- package/dist/templates/trellis/index.d.ts.map +1 -1
- package/dist/templates/trellis/index.js +4 -0
- package/dist/templates/trellis/index.js.map +1 -1
- package/dist/templates/trellis/scripts/add_session.py +52 -21
- package/dist/templates/trellis/scripts/common/__init__.py +3 -1
- package/dist/templates/trellis/scripts/common/cli_adapter.py +125 -20
- package/dist/templates/trellis/scripts/common/config.py +52 -0
- package/dist/templates/trellis/scripts/common/git_context.py +121 -11
- package/dist/templates/trellis/scripts/multi_agent/plan.py +4 -1
- package/dist/templates/trellis/scripts/multi_agent/start.py +5 -1
- package/dist/templates/trellis/scripts/task.py +26 -0
- package/dist/types/ai-tools.d.ts +3 -3
- package/dist/types/ai-tools.d.ts.map +1 -1
- package/dist/types/ai-tools.js +8 -0
- package/dist/types/ai-tools.js.map +1 -1
- package/dist/utils/proxy.d.ts +25 -0
- package/dist/utils/proxy.d.ts.map +1 -0
- package/dist/utils/proxy.js +60 -0
- package/dist/utils/proxy.js.map +1 -0
- package/dist/utils/template-fetcher.d.ts +11 -2
- package/dist/utils/template-fetcher.d.ts.map +1 -1
- package/dist/utils/template-fetcher.js +92 -19
- package/dist/utils/template-fetcher.js.map +1 -1
- package/dist/utils/template-hash.d.ts.map +1 -1
- package/dist/utils/template-hash.js +1 -0
- package/dist/utils/template-hash.js.map +1 -1
- package/package.json +10 -9
|
@@ -10,18 +10,8 @@ Provides:
|
|
|
10
10
|
|
|
11
11
|
from __future__ import annotations
|
|
12
12
|
|
|
13
|
-
import sys
|
|
14
|
-
|
|
15
|
-
# IMPORTANT: Force stdout to use UTF-8 on Windows
|
|
16
|
-
# This fixes UnicodeEncodeError when outputting non-ASCII characters
|
|
17
|
-
if sys.platform == "win32":
|
|
18
|
-
import io as _io
|
|
19
|
-
if hasattr(sys.stdout, "reconfigure"):
|
|
20
|
-
sys.stdout.reconfigure(encoding="utf-8", errors="replace") # type: ignore[union-attr]
|
|
21
|
-
elif hasattr(sys.stdout, "detach"):
|
|
22
|
-
sys.stdout = _io.TextIOWrapper(sys.stdout.detach(), encoding="utf-8", errors="replace") # type: ignore[union-attr]
|
|
23
|
-
|
|
24
13
|
import json
|
|
14
|
+
import sys
|
|
25
15
|
import subprocess
|
|
26
16
|
from pathlib import Path
|
|
27
17
|
|
|
@@ -345,6 +335,117 @@ def get_context_text(repo_root: Path | None = None) -> str:
|
|
|
345
335
|
return "\n".join(lines)
|
|
346
336
|
|
|
347
337
|
|
|
338
|
+
def get_context_text_record(repo_root: Path | None = None) -> str:
|
|
339
|
+
"""Get context as formatted text for record-session mode.
|
|
340
|
+
|
|
341
|
+
Focused output: MY ACTIVE TASKS first (with [!!!] emphasis),
|
|
342
|
+
then GIT STATUS, RECENT COMMITS, CURRENT TASK.
|
|
343
|
+
|
|
344
|
+
Args:
|
|
345
|
+
repo_root: Repository root path. Defaults to auto-detected.
|
|
346
|
+
|
|
347
|
+
Returns:
|
|
348
|
+
Formatted text output for record-session.
|
|
349
|
+
"""
|
|
350
|
+
if repo_root is None:
|
|
351
|
+
repo_root = get_repo_root()
|
|
352
|
+
|
|
353
|
+
lines: list[str] = []
|
|
354
|
+
lines.append("========================================")
|
|
355
|
+
lines.append("SESSION CONTEXT (RECORD MODE)")
|
|
356
|
+
lines.append("========================================")
|
|
357
|
+
lines.append("")
|
|
358
|
+
|
|
359
|
+
developer = get_developer(repo_root)
|
|
360
|
+
if not developer:
|
|
361
|
+
lines.append(
|
|
362
|
+
f"ERROR: Not initialized. Run: python3 ./{DIR_WORKFLOW}/{DIR_SCRIPTS}/init_developer.py <name>"
|
|
363
|
+
)
|
|
364
|
+
return "\n".join(lines)
|
|
365
|
+
|
|
366
|
+
# MY ACTIVE TASKS — first and prominent
|
|
367
|
+
lines.append(f"## [!!!] MY ACTIVE TASKS (Assigned to {developer})")
|
|
368
|
+
lines.append("[!] Review whether any should be archived before recording this session.")
|
|
369
|
+
lines.append("")
|
|
370
|
+
|
|
371
|
+
tasks_dir = get_tasks_dir(repo_root)
|
|
372
|
+
my_task_count = 0
|
|
373
|
+
|
|
374
|
+
if tasks_dir.is_dir():
|
|
375
|
+
for d in sorted(tasks_dir.iterdir()):
|
|
376
|
+
if d.is_dir() and d.name != "archive":
|
|
377
|
+
t_json = d / FILE_TASK_JSON
|
|
378
|
+
if t_json.is_file():
|
|
379
|
+
data = _read_json_file(t_json)
|
|
380
|
+
if data:
|
|
381
|
+
assignee = data.get("assignee", "")
|
|
382
|
+
status = data.get("status", "planning")
|
|
383
|
+
|
|
384
|
+
if assignee == developer:
|
|
385
|
+
title = data.get("title") or data.get("name") or "unknown"
|
|
386
|
+
priority = data.get("priority", "P2")
|
|
387
|
+
lines.append(f"- [{priority}] {title} ({status}) — {d.name}")
|
|
388
|
+
my_task_count += 1
|
|
389
|
+
|
|
390
|
+
if my_task_count == 0:
|
|
391
|
+
lines.append("(no active tasks assigned to you)")
|
|
392
|
+
lines.append("")
|
|
393
|
+
|
|
394
|
+
# GIT STATUS
|
|
395
|
+
lines.append("## GIT STATUS")
|
|
396
|
+
_, branch_out, _ = _run_git_command(["branch", "--show-current"], cwd=repo_root)
|
|
397
|
+
branch = branch_out.strip() or "unknown"
|
|
398
|
+
lines.append(f"Branch: {branch}")
|
|
399
|
+
|
|
400
|
+
_, status_out, _ = _run_git_command(["status", "--porcelain"], cwd=repo_root)
|
|
401
|
+
status_lines = [line for line in status_out.splitlines() if line.strip()]
|
|
402
|
+
status_count = len(status_lines)
|
|
403
|
+
|
|
404
|
+
if status_count == 0:
|
|
405
|
+
lines.append("Working directory: Clean")
|
|
406
|
+
else:
|
|
407
|
+
lines.append(f"Working directory: {status_count} uncommitted change(s)")
|
|
408
|
+
lines.append("")
|
|
409
|
+
lines.append("Changes:")
|
|
410
|
+
_, short_out, _ = _run_git_command(["status", "--short"], cwd=repo_root)
|
|
411
|
+
for line in short_out.splitlines()[:10]:
|
|
412
|
+
lines.append(line)
|
|
413
|
+
lines.append("")
|
|
414
|
+
|
|
415
|
+
# RECENT COMMITS
|
|
416
|
+
lines.append("## RECENT COMMITS")
|
|
417
|
+
_, log_out, _ = _run_git_command(["log", "--oneline", "-5"], cwd=repo_root)
|
|
418
|
+
if log_out.strip():
|
|
419
|
+
for line in log_out.splitlines():
|
|
420
|
+
lines.append(line)
|
|
421
|
+
else:
|
|
422
|
+
lines.append("(no commits)")
|
|
423
|
+
lines.append("")
|
|
424
|
+
|
|
425
|
+
# CURRENT TASK
|
|
426
|
+
lines.append("## CURRENT TASK")
|
|
427
|
+
current_task = get_current_task(repo_root)
|
|
428
|
+
if current_task:
|
|
429
|
+
current_task_dir = repo_root / current_task
|
|
430
|
+
task_json_path = current_task_dir / FILE_TASK_JSON
|
|
431
|
+
lines.append(f"Path: {current_task}")
|
|
432
|
+
|
|
433
|
+
if task_json_path.is_file():
|
|
434
|
+
data = _read_json_file(task_json_path)
|
|
435
|
+
if data:
|
|
436
|
+
t_name = data.get("name") or data.get("id") or "unknown"
|
|
437
|
+
t_status = data.get("status", "unknown")
|
|
438
|
+
lines.append(f"Name: {t_name}")
|
|
439
|
+
lines.append(f"Status: {t_status}")
|
|
440
|
+
else:
|
|
441
|
+
lines.append("(none)")
|
|
442
|
+
lines.append("")
|
|
443
|
+
|
|
444
|
+
lines.append("========================================")
|
|
445
|
+
|
|
446
|
+
return "\n".join(lines)
|
|
447
|
+
|
|
448
|
+
|
|
348
449
|
def output_text(repo_root: Path | None = None) -> None:
|
|
349
450
|
"""Output context in text format.
|
|
350
451
|
|
|
@@ -370,11 +471,20 @@ def main() -> None:
|
|
|
370
471
|
action="store_true",
|
|
371
472
|
help="Output context in JSON format",
|
|
372
473
|
)
|
|
474
|
+
parser.add_argument(
|
|
475
|
+
"--mode",
|
|
476
|
+
"-m",
|
|
477
|
+
choices=["default", "record"],
|
|
478
|
+
default="default",
|
|
479
|
+
help="Output mode: default (full context) or record (for record-session)",
|
|
480
|
+
)
|
|
373
481
|
|
|
374
482
|
args = parser.parse_args()
|
|
375
483
|
|
|
376
484
|
if args.json:
|
|
377
485
|
output_json()
|
|
486
|
+
elif args.mode == "record":
|
|
487
|
+
print(get_context_text_record())
|
|
378
488
|
else:
|
|
379
489
|
output_text()
|
|
380
490
|
|
|
@@ -77,7 +77,7 @@ def main() -> int:
|
|
|
77
77
|
parser.add_argument("--requirement", "-r", required=True, help="Requirement description")
|
|
78
78
|
parser.add_argument(
|
|
79
79
|
"--platform", "-p",
|
|
80
|
-
choices=["claude", "cursor", "iflow", "opencode"],
|
|
80
|
+
choices=["claude", "cursor", "iflow", "opencode", "qoder"],
|
|
81
81
|
default=DEFAULT_PLATFORM,
|
|
82
82
|
help="Platform to use (default: claude)"
|
|
83
83
|
)
|
|
@@ -173,6 +173,9 @@ def main() -> int:
|
|
|
173
173
|
env["http_proxy"] = http_proxy
|
|
174
174
|
env["all_proxy"] = all_proxy
|
|
175
175
|
|
|
176
|
+
# Clear nested-session detection so the new CLI process can start
|
|
177
|
+
env.pop("CLAUDECODE", None)
|
|
178
|
+
|
|
176
179
|
# Set non-interactive env var based on platform
|
|
177
180
|
env.update(adapter.get_non_interactive_env())
|
|
178
181
|
|
|
@@ -124,7 +124,7 @@ def main() -> int:
|
|
|
124
124
|
parser.add_argument("task_dir", help="Task directory path")
|
|
125
125
|
parser.add_argument(
|
|
126
126
|
"--platform", "-p",
|
|
127
|
-
choices=["claude", "cursor", "iflow", "opencode"],
|
|
127
|
+
choices=["claude", "cursor", "iflow", "opencode", "qoder"],
|
|
128
128
|
default=DEFAULT_PLATFORM,
|
|
129
129
|
help="Platform to use (default: claude)"
|
|
130
130
|
)
|
|
@@ -362,6 +362,10 @@ def main() -> int:
|
|
|
362
362
|
env["http_proxy"] = http_proxy
|
|
363
363
|
env["all_proxy"] = all_proxy
|
|
364
364
|
|
|
365
|
+
# Clear nested-session detection so the new CLI process can start
|
|
366
|
+
# (when this script runs inside a Claude Code session, CLAUDECODE=1 is inherited)
|
|
367
|
+
env.pop("CLAUDECODE", None)
|
|
368
|
+
|
|
365
369
|
# Set non-interactive env var based on platform
|
|
366
370
|
env.update(adapter.get_non_interactive_env())
|
|
367
371
|
|
|
@@ -659,6 +659,10 @@ def cmd_archive(args: argparse.Namespace) -> int:
|
|
|
659
659
|
year_month = archive_dest.parent.name
|
|
660
660
|
print(colored(f"Archived: {dir_name} -> archive/{year_month}/", Colors.GREEN), file=sys.stderr)
|
|
661
661
|
|
|
662
|
+
# Auto-commit unless --no-commit
|
|
663
|
+
if not getattr(args, "no_commit", False):
|
|
664
|
+
_auto_commit_archive(dir_name, repo_root)
|
|
665
|
+
|
|
662
666
|
# Return the archive path
|
|
663
667
|
print(f"{DIR_WORKFLOW}/{DIR_TASKS}/{DIR_ARCHIVE}/{year_month}/{dir_name}")
|
|
664
668
|
return 0
|
|
@@ -666,6 +670,27 @@ def cmd_archive(args: argparse.Namespace) -> int:
|
|
|
666
670
|
return 1
|
|
667
671
|
|
|
668
672
|
|
|
673
|
+
def _auto_commit_archive(task_name: str, repo_root: Path) -> None:
|
|
674
|
+
"""Stage .trellis/tasks/ changes and commit after archive."""
|
|
675
|
+
tasks_rel = f"{DIR_WORKFLOW}/{DIR_TASKS}"
|
|
676
|
+
_run_git_command(["add", "-A", tasks_rel], cwd=repo_root)
|
|
677
|
+
|
|
678
|
+
# Check if there are staged changes
|
|
679
|
+
rc, _, _ = _run_git_command(
|
|
680
|
+
["diff", "--cached", "--quiet", "--", tasks_rel], cwd=repo_root
|
|
681
|
+
)
|
|
682
|
+
if rc == 0:
|
|
683
|
+
print("[OK] No task changes to commit.", file=sys.stderr)
|
|
684
|
+
return
|
|
685
|
+
|
|
686
|
+
commit_msg = f"chore(task): archive {task_name}"
|
|
687
|
+
rc, _, err = _run_git_command(["commit", "-m", commit_msg], cwd=repo_root)
|
|
688
|
+
if rc == 0:
|
|
689
|
+
print(f"[OK] Auto-committed: {commit_msg}", file=sys.stderr)
|
|
690
|
+
else:
|
|
691
|
+
print(f"[WARN] Auto-commit failed: {err.strip()}", file=sys.stderr)
|
|
692
|
+
|
|
693
|
+
|
|
669
694
|
# =============================================================================
|
|
670
695
|
# Command: list
|
|
671
696
|
# =============================================================================
|
|
@@ -1005,6 +1030,7 @@ def main() -> int:
|
|
|
1005
1030
|
# archive
|
|
1006
1031
|
p_archive = subparsers.add_parser("archive", help="Archive task")
|
|
1007
1032
|
p_archive.add_argument("name", help="Task name")
|
|
1033
|
+
p_archive.add_argument("--no-commit", action="store_true", help="Skip auto git commit after archive")
|
|
1008
1034
|
|
|
1009
1035
|
# list
|
|
1010
1036
|
p_list = subparsers.add_parser("list", help="List tasks")
|
package/dist/types/ai-tools.d.ts
CHANGED
|
@@ -6,16 +6,16 @@
|
|
|
6
6
|
/**
|
|
7
7
|
* Supported AI coding tools
|
|
8
8
|
*/
|
|
9
|
-
export type AITool = "claude-code" | "cursor" | "opencode" | "iflow" | "codex" | "kilo" | "kiro" | "gemini" | "antigravity";
|
|
9
|
+
export type AITool = "claude-code" | "cursor" | "opencode" | "iflow" | "codex" | "kilo" | "kiro" | "gemini" | "antigravity" | "qoder";
|
|
10
10
|
/**
|
|
11
11
|
* Template directory categories
|
|
12
12
|
*/
|
|
13
|
-
export type TemplateDir = "common" | "claude" | "cursor" | "opencode" | "iflow" | "codex" | "kilo" | "kiro" | "gemini" | "antigravity";
|
|
13
|
+
export type TemplateDir = "common" | "claude" | "cursor" | "opencode" | "iflow" | "codex" | "kilo" | "kiro" | "gemini" | "antigravity" | "qoder";
|
|
14
14
|
/**
|
|
15
15
|
* CLI flag names for platform selection (e.g., --claude, --cursor, --kilo, --kiro, --gemini, --antigravity)
|
|
16
16
|
* Must match keys in InitOptions (src/commands/init.ts)
|
|
17
17
|
*/
|
|
18
|
-
export type CliFlag = "claude" | "cursor" | "opencode" | "iflow" | "codex" | "kilo" | "kiro" | "gemini" | "antigravity";
|
|
18
|
+
export type CliFlag = "claude" | "cursor" | "opencode" | "iflow" | "codex" | "kilo" | "kiro" | "gemini" | "antigravity" | "qoder";
|
|
19
19
|
/**
|
|
20
20
|
* Configuration for an AI tool
|
|
21
21
|
*/
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ai-tools.d.ts","sourceRoot":"","sources":["../../src/types/ai-tools.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH;;GAEG;AACH,MAAM,MAAM,MAAM,GACd,aAAa,GACb,QAAQ,GACR,UAAU,GACV,OAAO,GACP,OAAO,GACP,MAAM,GACN,MAAM,GACN,QAAQ,GACR,aAAa,CAAC;
|
|
1
|
+
{"version":3,"file":"ai-tools.d.ts","sourceRoot":"","sources":["../../src/types/ai-tools.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH;;GAEG;AACH,MAAM,MAAM,MAAM,GACd,aAAa,GACb,QAAQ,GACR,UAAU,GACV,OAAO,GACP,OAAO,GACP,MAAM,GACN,MAAM,GACN,QAAQ,GACR,aAAa,GACb,OAAO,CAAC;AAEZ;;GAEG;AACH,MAAM,MAAM,WAAW,GACnB,QAAQ,GACR,QAAQ,GACR,QAAQ,GACR,UAAU,GACV,OAAO,GACP,OAAO,GACP,MAAM,GACN,MAAM,GACN,QAAQ,GACR,aAAa,GACb,OAAO,CAAC;AAEZ;;;GAGG;AACH,MAAM,MAAM,OAAO,GACf,QAAQ,GACR,QAAQ,GACR,UAAU,GACV,OAAO,GACP,OAAO,GACP,MAAM,GACN,MAAM,GACN,QAAQ,GACR,aAAa,GACb,OAAO,CAAC;AAEZ;;GAEG;AACH,MAAM,WAAW,YAAY;IAC3B,+BAA+B;IAC/B,IAAI,EAAE,MAAM,CAAC;IACb,kDAAkD;IAClD,YAAY,EAAE,WAAW,EAAE,CAAC;IAC5B,kEAAkE;IAClE,SAAS,EAAE,MAAM,CAAC;IAClB,qEAAqE;IACrE,OAAO,EAAE,OAAO,CAAC;IACjB,yEAAyE;IACzE,cAAc,EAAE,OAAO,CAAC;IACxB,+EAA+E;IAC/E,cAAc,EAAE,OAAO,CAAC;CACzB;AAED;;;;;;;;;;GAUG;AACH,eAAO,MAAM,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,YAAY,CAiFjD,CAAC;AAEF;;GAEG;AACH,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,YAAY,CAExD;AAED;;GAEG;AACH,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,GAAG,WAAW,EAAE,CAE3D"}
|
package/dist/types/ai-tools.js
CHANGED
|
@@ -87,6 +87,14 @@ export const AI_TOOLS = {
|
|
|
87
87
|
defaultChecked: false,
|
|
88
88
|
hasPythonHooks: false,
|
|
89
89
|
},
|
|
90
|
+
qoder: {
|
|
91
|
+
name: "Qoder",
|
|
92
|
+
templateDirs: ["common", "qoder"],
|
|
93
|
+
configDir: ".qoder",
|
|
94
|
+
cliFlag: "qoder",
|
|
95
|
+
defaultChecked: false,
|
|
96
|
+
hasPythonHooks: false,
|
|
97
|
+
},
|
|
90
98
|
};
|
|
91
99
|
/**
|
|
92
100
|
* Get the configuration for a specific AI tool
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ai-tools.js","sourceRoot":"","sources":["../../src/types/ai-tools.ts"],"names":[],"mappings":"AAAA;;;;GAIG;
|
|
1
|
+
{"version":3,"file":"ai-tools.js","sourceRoot":"","sources":["../../src/types/ai-tools.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAmEH;;;;;;;;;;GAUG;AACH,MAAM,CAAC,MAAM,QAAQ,GAAiC;IACpD,aAAa,EAAE;QACb,IAAI,EAAE,aAAa;QACnB,YAAY,EAAE,CAAC,QAAQ,EAAE,QAAQ,CAAC;QAClC,SAAS,EAAE,SAAS;QACpB,OAAO,EAAE,QAAQ;QACjB,cAAc,EAAE,IAAI;QACpB,cAAc,EAAE,IAAI;KACrB;IACD,MAAM,EAAE;QACN,IAAI,EAAE,QAAQ;QACd,YAAY,EAAE,CAAC,QAAQ,EAAE,QAAQ,CAAC;QAClC,SAAS,EAAE,SAAS;QACpB,OAAO,EAAE,QAAQ;QACjB,cAAc,EAAE,IAAI;QACpB,cAAc,EAAE,KAAK;KACtB;IACD,QAAQ,EAAE;QACR,IAAI,EAAE,UAAU;QAChB,YAAY,EAAE,CAAC,QAAQ,EAAE,UAAU,CAAC;QACpC,SAAS,EAAE,WAAW;QACtB,OAAO,EAAE,UAAU;QACnB,cAAc,EAAE,KAAK;QACrB,cAAc,EAAE,KAAK;KACtB;IACD,KAAK,EAAE;QACL,IAAI,EAAE,WAAW;QACjB,YAAY,EAAE,CAAC,QAAQ,EAAE,OAAO,CAAC;QACjC,SAAS,EAAE,QAAQ;QACnB,OAAO,EAAE,OAAO;QAChB,cAAc,EAAE,KAAK;QACrB,cAAc,EAAE,IAAI;KACrB;IACD,KAAK,EAAE;QACL,IAAI,EAAE,OAAO;QACb,YAAY,EAAE,CAAC,QAAQ,EAAE,OAAO,CAAC;QACjC,SAAS,EAAE,gBAAgB;QAC3B,OAAO,EAAE,OAAO;QAChB,cAAc,EAAE,KAAK;QACrB,cAAc,EAAE,KAAK;KACtB;IACD,IAAI,EAAE;QACJ,IAAI,EAAE,UAAU;QAChB,YAAY,EAAE,CAAC,QAAQ,EAAE,MAAM,CAAC;QAChC,SAAS,EAAE,WAAW;QACtB,OAAO,EAAE,MAAM;QACf,cAAc,EAAE,KAAK;QACrB,cAAc,EAAE,KAAK;KACtB;IACD,IAAI,EAAE;QACJ,IAAI,EAAE,WAAW;QACjB,YAAY,EAAE,CAAC,QAAQ,EAAE,MAAM,CAAC;QAChC,SAAS,EAAE,cAAc;QACzB,OAAO,EAAE,MAAM;QACf,cAAc,EAAE,KAAK;QACrB,cAAc,EAAE,KAAK;KACtB;IACD,MAAM,EAAE;QACN,IAAI,EAAE,YAAY;QAClB,YAAY,EAAE,CAAC,QAAQ,EAAE,QAAQ,CAAC;QAClC,SAAS,EAAE,SAAS;QACpB,OAAO,EAAE,QAAQ;QACjB,cAAc,EAAE,KAAK;QACrB,cAAc,EAAE,KAAK;KACtB;IACD,WAAW,EAAE;QACX,IAAI,EAAE,aAAa;QACnB,YAAY,EAAE,CAAC,QAAQ,EAAE,aAAa,CAAC;QACvC,SAAS,EAAE,kBAAkB;QAC7B,OAAO,EAAE,aAAa;QACtB,cAAc,EAAE,KAAK;QACrB,cAAc,EAAE,KAAK;KACtB;IACD,KAAK,EAAE;QACL,IAAI,EAAE,OAAO;QACb,YAAY,EAAE,CAAC,QAAQ,EAAE,OAAO,CAAC;QACjC,SAAS,EAAE,QAAQ;QACnB,OAAO,EAAE,OAAO;QAChB,cAAc,EAAE,KAAK;QACrB,cAAc,EAAE,KAAK;KACtB;CACF,CAAC;AAEF;;GAEG;AACH,MAAM,UAAU,aAAa,CAAC,IAAY;IACxC,OAAO,QAAQ,CAAC,IAAI,CAAC,CAAC;AACxB,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,eAAe,CAAC,IAAY;IAC1C,OAAO,QAAQ,CAAC,IAAI,CAAC,CAAC,YAAY,CAAC;AACrC,CAAC"}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Proxy detection and configuration for network requests.
|
|
3
|
+
*
|
|
4
|
+
* Detects HTTP_PROXY / HTTPS_PROXY / ALL_PROXY environment variables
|
|
5
|
+
* and configures undici's global dispatcher so that all fetch() calls
|
|
6
|
+
* (including those made internally by giget) go through the proxy.
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* Mask credentials in a proxy URL for safe logging.
|
|
10
|
+
*
|
|
11
|
+
* Replaces username and password with "***" so that credentials
|
|
12
|
+
* are never printed to the console or written to log files.
|
|
13
|
+
*/
|
|
14
|
+
export declare function maskProxyUrl(url: string): string;
|
|
15
|
+
/**
|
|
16
|
+
* Set up a global proxy dispatcher if proxy environment variables are present.
|
|
17
|
+
*
|
|
18
|
+
* Uses undici's ProxyAgent and setGlobalDispatcher so that all fetch() calls
|
|
19
|
+
* go through the proxy. The try/catch handles malformed proxy URLs (e.g.
|
|
20
|
+
* socks5://, missing protocol) that would cause `new ProxyAgent()` to throw.
|
|
21
|
+
*
|
|
22
|
+
* @returns The proxy URL string if a proxy was configured, or null otherwise.
|
|
23
|
+
*/
|
|
24
|
+
export declare function setupProxy(): string | null;
|
|
25
|
+
//# sourceMappingURL=proxy.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"proxy.d.ts","sourceRoot":"","sources":["../../src/utils/proxy.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAIH;;;;;GAKG;AACH,wBAAgB,YAAY,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAWhD;AAED;;;;;;;;GAQG;AACH,wBAAgB,UAAU,IAAI,MAAM,GAAG,IAAI,CAyB1C"}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Proxy detection and configuration for network requests.
|
|
3
|
+
*
|
|
4
|
+
* Detects HTTP_PROXY / HTTPS_PROXY / ALL_PROXY environment variables
|
|
5
|
+
* and configures undici's global dispatcher so that all fetch() calls
|
|
6
|
+
* (including those made internally by giget) go through the proxy.
|
|
7
|
+
*/
|
|
8
|
+
import { ProxyAgent, setGlobalDispatcher } from "undici";
|
|
9
|
+
/**
|
|
10
|
+
* Mask credentials in a proxy URL for safe logging.
|
|
11
|
+
*
|
|
12
|
+
* Replaces username and password with "***" so that credentials
|
|
13
|
+
* are never printed to the console or written to log files.
|
|
14
|
+
*/
|
|
15
|
+
export function maskProxyUrl(url) {
|
|
16
|
+
try {
|
|
17
|
+
const parsed = new URL(url);
|
|
18
|
+
if (parsed.username || parsed.password) {
|
|
19
|
+
parsed.username = "***";
|
|
20
|
+
parsed.password = "***";
|
|
21
|
+
}
|
|
22
|
+
return parsed.toString();
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
return "***";
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Set up a global proxy dispatcher if proxy environment variables are present.
|
|
30
|
+
*
|
|
31
|
+
* Uses undici's ProxyAgent and setGlobalDispatcher so that all fetch() calls
|
|
32
|
+
* go through the proxy. The try/catch handles malformed proxy URLs (e.g.
|
|
33
|
+
* socks5://, missing protocol) that would cause `new ProxyAgent()` to throw.
|
|
34
|
+
*
|
|
35
|
+
* @returns The proxy URL string if a proxy was configured, or null otherwise.
|
|
36
|
+
*/
|
|
37
|
+
export function setupProxy() {
|
|
38
|
+
const candidates = [
|
|
39
|
+
process.env.HTTPS_PROXY,
|
|
40
|
+
process.env.https_proxy,
|
|
41
|
+
process.env.HTTP_PROXY,
|
|
42
|
+
process.env.http_proxy,
|
|
43
|
+
process.env.ALL_PROXY,
|
|
44
|
+
process.env.all_proxy,
|
|
45
|
+
];
|
|
46
|
+
const proxyUrl = candidates.find((v) => v != null && v !== "");
|
|
47
|
+
if (!proxyUrl) {
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
try {
|
|
51
|
+
const agent = new ProxyAgent(proxyUrl);
|
|
52
|
+
setGlobalDispatcher(agent);
|
|
53
|
+
return proxyUrl;
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
console.warn("Warning: Could not configure proxy. The proxy URL may be malformed.");
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
//# sourceMappingURL=proxy.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"proxy.js","sourceRoot":"","sources":["../../src/utils/proxy.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,EAAE,UAAU,EAAE,mBAAmB,EAAE,MAAM,QAAQ,CAAC;AAEzD;;;;;GAKG;AACH,MAAM,UAAU,YAAY,CAAC,GAAW;IACtC,IAAI,CAAC;QACH,MAAM,MAAM,GAAG,IAAI,GAAG,CAAC,GAAG,CAAC,CAAC;QAC5B,IAAI,MAAM,CAAC,QAAQ,IAAI,MAAM,CAAC,QAAQ,EAAE,CAAC;YACvC,MAAM,CAAC,QAAQ,GAAG,KAAK,CAAC;YACxB,MAAM,CAAC,QAAQ,GAAG,KAAK,CAAC;QAC1B,CAAC;QACD,OAAO,MAAM,CAAC,QAAQ,EAAE,CAAC;IAC3B,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,KAAK,CAAC;IACf,CAAC;AACH,CAAC;AAED;;;;;;;;GAQG;AACH,MAAM,UAAU,UAAU;IACxB,MAAM,UAAU,GAAG;QACjB,OAAO,CAAC,GAAG,CAAC,WAAW;QACvB,OAAO,CAAC,GAAG,CAAC,WAAW;QACvB,OAAO,CAAC,GAAG,CAAC,UAAU;QACtB,OAAO,CAAC,GAAG,CAAC,UAAU;QACtB,OAAO,CAAC,GAAG,CAAC,SAAS;QACrB,OAAO,CAAC,GAAG,CAAC,SAAS;KACtB,CAAC;IACF,MAAM,QAAQ,GAAG,UAAU,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,IAAI,IAAI,IAAI,CAAC,KAAK,EAAE,CAAC,CAAC;IAE/D,IAAI,CAAC,QAAQ,EAAE,CAAC;QACd,OAAO,IAAI,CAAC;IACd,CAAC;IAED,IAAI,CAAC;QACH,MAAM,KAAK,GAAG,IAAI,UAAU,CAAC,QAAQ,CAAC,CAAC;QACvC,mBAAmB,CAAC,KAAK,CAAC,CAAC;QAC3B,OAAO,QAAQ,CAAC;IAClB,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,CAAC,IAAI,CACV,qEAAqE,CACtE,CAAC;QACF,OAAO,IAAI,CAAC;IACd,CAAC;AACH,CAAC"}
|
|
@@ -4,6 +4,14 @@
|
|
|
4
4
|
* Fetches spec templates from the official docs repository:
|
|
5
5
|
* https://github.com/mindfold-ai/docs/tree/main/marketplace
|
|
6
6
|
*/
|
|
7
|
+
export declare const TEMPLATE_INDEX_URL = "https://raw.githubusercontent.com/mindfold-ai/docs/main/marketplace/index.json";
|
|
8
|
+
/** Timeout constants for network operations */
|
|
9
|
+
export declare const TIMEOUTS: {
|
|
10
|
+
/** Timeout for fetching the template index (ms) */
|
|
11
|
+
readonly INDEX_FETCH_MS: 5000;
|
|
12
|
+
/** Timeout for downloading a template via giget (ms) */
|
|
13
|
+
readonly DOWNLOAD_MS: 30000;
|
|
14
|
+
};
|
|
7
15
|
export interface SpecTemplate {
|
|
8
16
|
id: string;
|
|
9
17
|
type: string;
|
|
@@ -15,7 +23,7 @@ export interface SpecTemplate {
|
|
|
15
23
|
export type TemplateStrategy = "skip" | "overwrite" | "append";
|
|
16
24
|
/**
|
|
17
25
|
* Fetch available templates from the remote index
|
|
18
|
-
* Returns empty array on network error (allows fallback to blank)
|
|
26
|
+
* Returns empty array on network error or timeout (allows fallback to blank)
|
|
19
27
|
*/
|
|
20
28
|
export declare function fetchTemplateIndex(): Promise<SpecTemplate[]>;
|
|
21
29
|
/**
|
|
@@ -41,9 +49,10 @@ export declare function downloadWithStrategy(templatePath: string, destDir: stri
|
|
|
41
49
|
* @param cwd - Current working directory
|
|
42
50
|
* @param templateId - Template ID from the index
|
|
43
51
|
* @param strategy - How to handle existing directory
|
|
52
|
+
* @param template - Optional pre-fetched SpecTemplate to avoid double-fetch
|
|
44
53
|
* @returns Object with success status and message
|
|
45
54
|
*/
|
|
46
|
-
export declare function downloadTemplateById(cwd: string, templateId: string, strategy: TemplateStrategy): Promise<{
|
|
55
|
+
export declare function downloadTemplateById(cwd: string, templateId: string, strategy: TemplateStrategy, template?: SpecTemplate): Promise<{
|
|
47
56
|
success: boolean;
|
|
48
57
|
message: string;
|
|
49
58
|
skipped?: boolean;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"template-fetcher.d.ts","sourceRoot":"","sources":["../../src/utils/template-fetcher.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;
|
|
1
|
+
{"version":3,"file":"template-fetcher.d.ts","sourceRoot":"","sources":["../../src/utils/template-fetcher.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAWH,eAAO,MAAM,kBAAkB,mFACmD,CAAC;AAYnF,+CAA+C;AAC/C,eAAO,MAAM,QAAQ;IACnB,mDAAmD;;IAEnD,wDAAwD;;CAEhD,CAAC;AAMX,MAAM,WAAW,YAAY;IAC3B,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE,MAAM,EAAE,CAAC;CACjB;AAOD,MAAM,MAAM,gBAAgB,GAAG,MAAM,GAAG,WAAW,GAAG,QAAQ,CAAC;AAgC/D;;;GAGG;AACH,wBAAsB,kBAAkB,IAAI,OAAO,CAAC,YAAY,EAAE,CAAC,CAclE;AAED;;GAEG;AACH,wBAAsB,YAAY,CAChC,UAAU,EAAE,MAAM,GACjB,OAAO,CAAC,YAAY,GAAG,IAAI,CAAC,CAG9B;AAMD;;GAEG;AACH,wBAAgB,cAAc,CAAC,GAAG,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,GAAG,MAAM,CAGxE;AAED;;;;;;;GAOG;AACH,wBAAsB,oBAAoB,CACxC,YAAY,EAAE,MAAM,EACpB,OAAO,EAAE,MAAM,EACf,QAAQ,EAAE,gBAAgB,GACzB,OAAO,CAAC,OAAO,CAAC,CAuElB;AA2BD;;;;;;;;GAQG;AACH,wBAAsB,oBAAoB,CACxC,GAAG,EAAE,MAAM,EACX,UAAU,EAAE,MAAM,EAClB,QAAQ,EAAE,gBAAgB,EAC1B,QAAQ,CAAC,EAAE,YAAY,GACtB,OAAO,CAAC;IAAE,OAAO,EAAE,OAAO,CAAC;IAAC,OAAO,EAAE,MAAM,CAAC;IAAC,OAAO,CAAC,EAAE,OAAO,CAAA;CAAE,CAAC,CAgEnE"}
|
|
@@ -11,7 +11,7 @@ import { downloadTemplate } from "giget";
|
|
|
11
11
|
// =============================================================================
|
|
12
12
|
// Constants
|
|
13
13
|
// =============================================================================
|
|
14
|
-
const TEMPLATE_INDEX_URL = "https://raw.githubusercontent.com/mindfold-ai/docs/main/marketplace/index.json";
|
|
14
|
+
export const TEMPLATE_INDEX_URL = "https://raw.githubusercontent.com/mindfold-ai/docs/main/marketplace/index.json";
|
|
15
15
|
const TEMPLATE_REPO = "gh:mindfold-ai/docs";
|
|
16
16
|
/** Map template type to installation path */
|
|
17
17
|
const INSTALL_PATHS = {
|
|
@@ -20,16 +20,42 @@ const INSTALL_PATHS = {
|
|
|
20
20
|
command: ".claude/commands",
|
|
21
21
|
full: ".", // Entire project root
|
|
22
22
|
};
|
|
23
|
+
/** Timeout constants for network operations */
|
|
24
|
+
export const TIMEOUTS = {
|
|
25
|
+
/** Timeout for fetching the template index (ms) */
|
|
26
|
+
INDEX_FETCH_MS: 5_000,
|
|
27
|
+
/** Timeout for downloading a template via giget (ms) */
|
|
28
|
+
DOWNLOAD_MS: 30_000,
|
|
29
|
+
};
|
|
30
|
+
// =============================================================================
|
|
31
|
+
// Helpers
|
|
32
|
+
// =============================================================================
|
|
33
|
+
/**
|
|
34
|
+
* Race a promise against a timeout.
|
|
35
|
+
* giget does not support AbortSignal, so we use Promise.race instead.
|
|
36
|
+
* The timer is cleaned up on success to avoid keeping the process alive.
|
|
37
|
+
*/
|
|
38
|
+
function withTimeout(promise, ms, label) {
|
|
39
|
+
let timer;
|
|
40
|
+
const timeoutPromise = new Promise((_, reject) => {
|
|
41
|
+
timer = setTimeout(() => reject(new Error(`${label} timed out after ${ms / 1000}s`)), ms);
|
|
42
|
+
});
|
|
43
|
+
return Promise.race([promise, timeoutPromise]).finally(() => {
|
|
44
|
+
clearTimeout(timer);
|
|
45
|
+
});
|
|
46
|
+
}
|
|
23
47
|
// =============================================================================
|
|
24
48
|
// Fetch Template Index
|
|
25
49
|
// =============================================================================
|
|
26
50
|
/**
|
|
27
51
|
* Fetch available templates from the remote index
|
|
28
|
-
* Returns empty array on network error (allows fallback to blank)
|
|
52
|
+
* Returns empty array on network error or timeout (allows fallback to blank)
|
|
29
53
|
*/
|
|
30
54
|
export async function fetchTemplateIndex() {
|
|
31
55
|
try {
|
|
32
|
-
const res = await fetch(TEMPLATE_INDEX_URL
|
|
56
|
+
const res = await fetch(TEMPLATE_INDEX_URL, {
|
|
57
|
+
signal: AbortSignal.timeout(TIMEOUTS.INDEX_FETCH_MS),
|
|
58
|
+
});
|
|
33
59
|
if (!res.ok) {
|
|
34
60
|
throw new Error(`HTTP ${res.status}`);
|
|
35
61
|
}
|
|
@@ -37,7 +63,7 @@ export async function fetchTemplateIndex() {
|
|
|
37
63
|
return index.templates;
|
|
38
64
|
}
|
|
39
65
|
catch {
|
|
40
|
-
// Network error - return empty array, caller will fallback to blank
|
|
66
|
+
// Network error or timeout - return empty array, caller will fallback to blank
|
|
41
67
|
return [];
|
|
42
68
|
}
|
|
43
69
|
}
|
|
@@ -80,12 +106,27 @@ export async function downloadWithStrategy(templatePath, destDir, strategy) {
|
|
|
80
106
|
if (strategy === "append" && exists) {
|
|
81
107
|
const tempDir = path.join(os.tmpdir(), `trellis-template-${Date.now()}`);
|
|
82
108
|
try {
|
|
83
|
-
await downloadTemplate(`${TEMPLATE_REPO}/${templatePath}`, {
|
|
109
|
+
await withTimeout(downloadTemplate(`${TEMPLATE_REPO}/${templatePath}`, {
|
|
84
110
|
dir: tempDir,
|
|
85
111
|
preferOffline: true,
|
|
86
|
-
});
|
|
112
|
+
}), TIMEOUTS.DOWNLOAD_MS, "Template download");
|
|
87
113
|
await copyMissing(tempDir, destDir);
|
|
88
114
|
}
|
|
115
|
+
catch (error) {
|
|
116
|
+
// Clean up partially written files on timeout.
|
|
117
|
+
// Note: giget does not support AbortSignal, so the background download may
|
|
118
|
+
// still be running. Removing the directory causes it to fail with ENOENT,
|
|
119
|
+
// which settles the orphaned promise harmlessly.
|
|
120
|
+
if (error instanceof Error && error.message.includes("timed out")) {
|
|
121
|
+
try {
|
|
122
|
+
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
123
|
+
}
|
|
124
|
+
catch {
|
|
125
|
+
// Best-effort cleanup
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
throw error;
|
|
129
|
+
}
|
|
89
130
|
finally {
|
|
90
131
|
// Clean up temp directory
|
|
91
132
|
await fs.promises.rm(tempDir, { recursive: true, force: true });
|
|
@@ -93,10 +134,27 @@ export async function downloadWithStrategy(templatePath, destDir, strategy) {
|
|
|
93
134
|
return true;
|
|
94
135
|
}
|
|
95
136
|
// Default: Direct download (for new directory or after overwrite)
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
137
|
+
try {
|
|
138
|
+
await withTimeout(downloadTemplate(`${TEMPLATE_REPO}/${templatePath}`, {
|
|
139
|
+
dir: destDir,
|
|
140
|
+
preferOffline: true,
|
|
141
|
+
}), TIMEOUTS.DOWNLOAD_MS, "Template download");
|
|
142
|
+
}
|
|
143
|
+
catch (error) {
|
|
144
|
+
// Clean up partially written files on timeout.
|
|
145
|
+
// Note: giget does not support AbortSignal, so the background download may
|
|
146
|
+
// still be running. Removing the directory causes it to fail with ENOENT,
|
|
147
|
+
// which settles the orphaned promise harmlessly.
|
|
148
|
+
if (error instanceof Error && error.message.includes("timed out")) {
|
|
149
|
+
try {
|
|
150
|
+
fs.rmSync(destDir, { recursive: true, force: true });
|
|
151
|
+
}
|
|
152
|
+
catch {
|
|
153
|
+
// Best-effort cleanup
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
throw error;
|
|
157
|
+
}
|
|
100
158
|
return true;
|
|
101
159
|
}
|
|
102
160
|
/**
|
|
@@ -127,26 +185,27 @@ async function copyMissing(src, dest) {
|
|
|
127
185
|
* @param cwd - Current working directory
|
|
128
186
|
* @param templateId - Template ID from the index
|
|
129
187
|
* @param strategy - How to handle existing directory
|
|
188
|
+
* @param template - Optional pre-fetched SpecTemplate to avoid double-fetch
|
|
130
189
|
* @returns Object with success status and message
|
|
131
190
|
*/
|
|
132
|
-
export async function downloadTemplateById(cwd, templateId, strategy) {
|
|
133
|
-
//
|
|
134
|
-
const
|
|
135
|
-
if (!
|
|
191
|
+
export async function downloadTemplateById(cwd, templateId, strategy, template) {
|
|
192
|
+
// Use pre-fetched template or find from index
|
|
193
|
+
const resolved = template ?? (await findTemplate(templateId));
|
|
194
|
+
if (!resolved) {
|
|
136
195
|
return {
|
|
137
196
|
success: false,
|
|
138
197
|
message: `Template "${templateId}" not found`,
|
|
139
198
|
};
|
|
140
199
|
}
|
|
141
200
|
// Only support spec type in MVP
|
|
142
|
-
if (
|
|
201
|
+
if (resolved.type !== "spec") {
|
|
143
202
|
return {
|
|
144
203
|
success: false,
|
|
145
|
-
message: `Template type "${
|
|
204
|
+
message: `Template type "${resolved.type}" is not supported yet (only "spec" is supported)`,
|
|
146
205
|
};
|
|
147
206
|
}
|
|
148
207
|
// Get destination path
|
|
149
|
-
const destDir = getInstallPath(cwd,
|
|
208
|
+
const destDir = getInstallPath(cwd, resolved.type);
|
|
150
209
|
// Check if directory exists for skip strategy
|
|
151
210
|
if (strategy === "skip" && fs.existsSync(destDir)) {
|
|
152
211
|
return {
|
|
@@ -157,7 +216,7 @@ export async function downloadTemplateById(cwd, templateId, strategy) {
|
|
|
157
216
|
}
|
|
158
217
|
// Download template
|
|
159
218
|
try {
|
|
160
|
-
await downloadWithStrategy(
|
|
219
|
+
await downloadWithStrategy(resolved.path, destDir, strategy);
|
|
161
220
|
return {
|
|
162
221
|
success: true,
|
|
163
222
|
message: `Downloaded template "${templateId}" to ${destDir}`,
|
|
@@ -165,9 +224,23 @@ export async function downloadTemplateById(cwd, templateId, strategy) {
|
|
|
165
224
|
}
|
|
166
225
|
catch (error) {
|
|
167
226
|
const errorMessage = error instanceof Error ? error.message : "Unknown error";
|
|
227
|
+
// Classify errors for user-friendly messages
|
|
228
|
+
if (errorMessage.includes("timed out")) {
|
|
229
|
+
return {
|
|
230
|
+
success: false,
|
|
231
|
+
message: "Download timed out. Check your network connection and try again.",
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
if (errorMessage.includes("Failed to download") ||
|
|
235
|
+
errorMessage.includes("Failed to fetch")) {
|
|
236
|
+
return {
|
|
237
|
+
success: false,
|
|
238
|
+
message: "Could not reach template server. Check your network connection.",
|
|
239
|
+
};
|
|
240
|
+
}
|
|
168
241
|
return {
|
|
169
242
|
success: false,
|
|
170
|
-
message: `
|
|
243
|
+
message: `Download failed: ${errorMessage}`,
|
|
171
244
|
};
|
|
172
245
|
}
|
|
173
246
|
}
|