@torus-engineering/tas-kit 1.13.0 → 2.1.0
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/.tas/_platform/claude-code/settings.json +58 -46
- package/.tas/_platform/hooks/code-quality.js +127 -127
- package/.tas/_platform/hooks/session-end.js +111 -111
- package/.tas/agents/architect.md +53 -53
- package/.tas/agents/aws-reviewer.md +71 -71
- package/.tas/agents/build-resolver.md +89 -59
- package/.tas/agents/code-explorer.md +63 -63
- package/.tas/agents/csharp-reviewer.md +62 -62
- package/.tas/agents/database-reviewer.md +73 -73
- package/.tas/agents/doc-updater.md +68 -66
- package/.tas/agents/python-reviewer.md +67 -67
- package/.tas/agents/security-reviewer.md +79 -79
- package/.tas/agents/software-engineer.md +53 -0
- package/.tas/agents/typescript-reviewer.md +65 -65
- package/.tas/commands/ado-create.md +33 -28
- package/.tas/commands/ado-delete.md +26 -22
- package/.tas/commands/ado-get.md +24 -20
- package/.tas/commands/ado-status.md +22 -18
- package/.tas/commands/ado-update.md +31 -27
- package/.tas/commands/tas-adr.md +37 -33
- package/.tas/commands/tas-apitest-plan.md +177 -173
- package/.tas/commands/tas-apitest.md +147 -143
- package/.tas/commands/tas-brainstorm.md +23 -19
- package/.tas/commands/tas-brd.md +50 -0
- package/.tas/commands/tas-bug.md +127 -113
- package/.tas/commands/tas-checklist.md +180 -0
- package/.tas/commands/tas-debug.md +103 -0
- package/.tas/commands/tas-design.md +41 -37
- package/.tas/commands/tas-dev.md +225 -125
- package/.tas/commands/tas-e2e-mobile.md +146 -155
- package/.tas/commands/tas-e2e-web.md +150 -163
- package/.tas/commands/tas-e2e.md +289 -102
- package/.tas/commands/tas-feature.md +181 -47
- package/.tas/commands/tas-fix.md +72 -51
- package/.tas/commands/tas-functest-mobile.md +138 -144
- package/.tas/commands/tas-functest-web.md +176 -192
- package/.tas/commands/tas-functest.md +225 -76
- package/.tas/commands/tas-init.md +22 -17
- package/.tas/commands/tas-master-plan.md +300 -0
- package/.tas/commands/tas-orchestrate.md +159 -0
- package/.tas/commands/tas-plan.md +152 -117
- package/.tas/commands/tas-prd.md +57 -37
- package/.tas/commands/tas-review-pr.md +174 -0
- package/.tas/commands/tas-review.md +115 -113
- package/.tas/commands/tas-sad.md +47 -43
- package/.tas/commands/tas-security.md +91 -87
- package/.tas/commands/tas-spec.md +54 -50
- package/.tas/commands/tas-status.md +25 -16
- package/.tas/project-status-example.yaml +3 -1
- package/.tas/rules/ado-integration.md +67 -65
- package/.tas/rules/common/api-design.md +517 -517
- package/.tas/rules/common/build-debug-loop.md +233 -0
- package/.tas/rules/common/code-review.md +4 -0
- package/.tas/rules/common/feature-done.md +42 -0
- package/.tas/rules/common/post-implementation-review.md +4 -0
- package/.tas/rules/common/project-status.md +33 -16
- package/.tas/rules/common/sad-impact.md +81 -0
- package/.tas/rules/common/tdd.md +104 -89
- package/.tas/rules/csharp/api-testing.md +2 -2
- package/.tas/rules/csharp/torus-core-framework.md +128 -0
- package/.tas/tas-example.yaml +9 -32
- package/.tas/templates/AGENTS.md +13 -0
- package/.tas/templates/API-Test-Spec.md +5 -4
- package/.tas/templates/BRD.md +133 -0
- package/.tas/templates/Bug.md +15 -0
- package/.tas/templates/E2E-Execution-Report.md +8 -8
- package/.tas/templates/E2E-Mobile-Spec.md +6 -8
- package/.tas/templates/E2E-Report.md +2 -2
- package/.tas/templates/E2E-Scenario.md +22 -22
- package/.tas/templates/E2E-Test-Spec.md +274 -0
- package/.tas/templates/E2E-Web-Spec.md +4 -4
- package/.tas/templates/Feature-Technical-Part.md +69 -0
- package/.tas/templates/Feature-Technical-Stack.md +74 -0
- package/.tas/templates/Feature-Technical.md +329 -0
- package/.tas/templates/Feature.md +50 -26
- package/.tas/templates/Func-Test-Script.md +29 -56
- package/.tas/templates/Func-Test-Spec.md +144 -142
- package/.tas/templates/PRD.md +173 -142
- package/.tas/templates/TestChecklist.md +96 -0
- package/.tas/templates/torus-dotnet-bootstrap.md +223 -0
- package/.tas/tools/tas-ado-readme.md +24 -27
- package/.tas/tools/tas-ado.py +328 -25
- package/.tas/tools/tas-github.py +339 -0
- package/README.md +142 -57
- package/bin/cli.js +90 -90
- package/lib/adapters/antigravity.js +131 -131
- package/lib/adapters/claude-code.js +71 -35
- package/lib/adapters/codex.js +157 -157
- package/lib/adapters/cursor.js +80 -80
- package/lib/adapters/index.js +20 -20
- package/lib/adapters/utils.js +81 -81
- package/lib/deleted-files.json +7 -0
- package/lib/install.js +546 -543
- package/package.json +2 -2
- package/.tas/README.md +0 -334
- package/.tas/commands/tas-epic.md +0 -35
- package/.tas/commands/tas-story.md +0 -91
- package/.tas/rules/common/story-done.md +0 -30
- package/.tas/templates/Epic.md +0 -46
- package/.tas/templates/Story.md +0 -90
|
@@ -0,0 +1,339 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
TAS GitHub Integration Script
|
|
4
|
+
GitHub PR operations for tas-review-pr command.
|
|
5
|
+
|
|
6
|
+
Usage: python tools/tas-github.py <command> [arguments]
|
|
7
|
+
|
|
8
|
+
Commands:
|
|
9
|
+
pr-get <pr-id>
|
|
10
|
+
pr-diff <pr-id>
|
|
11
|
+
pr-comment <pr-id> --comment <text>
|
|
12
|
+
pr-inline <pr-id> --file <path> --line <n> --comment <text>
|
|
13
|
+
pr-vote <pr-id> --vote <approve|reject|wait-for-author|reset>
|
|
14
|
+
|
|
15
|
+
Prerequisites:
|
|
16
|
+
- GitHub CLI (gh) installed and authenticated: gh auth login
|
|
17
|
+
- Python 3.8+
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
import argparse
|
|
21
|
+
import json
|
|
22
|
+
import os
|
|
23
|
+
import re
|
|
24
|
+
import subprocess
|
|
25
|
+
import sys
|
|
26
|
+
import tempfile
|
|
27
|
+
import urllib.error
|
|
28
|
+
import urllib.request
|
|
29
|
+
from pathlib import Path
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
# --- Helpers ---
|
|
33
|
+
|
|
34
|
+
def find_repo_root():
|
|
35
|
+
path = Path.cwd()
|
|
36
|
+
while path != path.parent:
|
|
37
|
+
if (path / ".git").exists():
|
|
38
|
+
return path
|
|
39
|
+
path = path.parent
|
|
40
|
+
print("ERROR: Not inside a git repository.")
|
|
41
|
+
sys.exit(1)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def get_remote_url(root):
|
|
45
|
+
result = subprocess.run(
|
|
46
|
+
["git", "remote", "get-url", "origin"],
|
|
47
|
+
capture_output=True,
|
|
48
|
+
cwd=str(root),
|
|
49
|
+
)
|
|
50
|
+
if result.returncode != 0:
|
|
51
|
+
print("ERROR: Cannot get git remote URL. Run inside a git repo with 'origin' remote.")
|
|
52
|
+
sys.exit(1)
|
|
53
|
+
return result.stdout.decode("utf-8").strip()
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def parse_owner_repo(remote_url):
|
|
57
|
+
"""Extract owner/repo from GitHub remote URL (https or ssh)."""
|
|
58
|
+
# ssh: git@github.com:owner/repo.git
|
|
59
|
+
m = re.search(r"github\.com[:/](.+?)(?:\.git)?$", remote_url)
|
|
60
|
+
if m:
|
|
61
|
+
return m.group(1).rstrip("/")
|
|
62
|
+
print(f"ERROR: Cannot parse owner/repo from remote URL: {remote_url}")
|
|
63
|
+
sys.exit(1)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def get_github_token():
|
|
67
|
+
result = subprocess.run(
|
|
68
|
+
["gh", "auth", "token"],
|
|
69
|
+
capture_output=True,
|
|
70
|
+
)
|
|
71
|
+
if result.returncode != 0:
|
|
72
|
+
print("ERROR: gh CLI not authenticated. Run: gh auth login")
|
|
73
|
+
sys.exit(1)
|
|
74
|
+
return result.stdout.decode("utf-8").strip()
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def gh_cmd(args):
|
|
78
|
+
"""Run gh CLI command and return parsed JSON output."""
|
|
79
|
+
cmd = ["gh"] + args + ["--output", "json"] if "--output" not in args else ["gh"] + args
|
|
80
|
+
result = subprocess.run(cmd, capture_output=True)
|
|
81
|
+
|
|
82
|
+
def _decode(b):
|
|
83
|
+
try:
|
|
84
|
+
return (b or b"").decode("utf-8")
|
|
85
|
+
except UnicodeDecodeError:
|
|
86
|
+
return (b or b"").decode("cp1252", errors="replace")
|
|
87
|
+
|
|
88
|
+
if result.returncode != 0:
|
|
89
|
+
print(f"ERROR: gh command failed:\n{_decode(result.stderr)}")
|
|
90
|
+
sys.exit(1)
|
|
91
|
+
stdout = _decode(result.stdout).strip()
|
|
92
|
+
if not stdout:
|
|
93
|
+
return {}
|
|
94
|
+
json_start = re.search(r"[{\[]", stdout)
|
|
95
|
+
if json_start:
|
|
96
|
+
try:
|
|
97
|
+
return json.loads(stdout[json_start.start():])
|
|
98
|
+
except json.JSONDecodeError:
|
|
99
|
+
return {}
|
|
100
|
+
return {}
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def github_rest(method, path, data, token):
|
|
104
|
+
"""Call GitHub REST API using token auth."""
|
|
105
|
+
import base64
|
|
106
|
+
url = f"https://api.github.com{path}"
|
|
107
|
+
bearer = f"Bearer {token}"
|
|
108
|
+
body = json.dumps(data).encode("utf-8")
|
|
109
|
+
req = urllib.request.Request(url, data=body, method=method)
|
|
110
|
+
req.add_header("Authorization", bearer)
|
|
111
|
+
req.add_header("Content-Type", "application/json")
|
|
112
|
+
req.add_header("Accept", "application/vnd.github+json")
|
|
113
|
+
req.add_header("X-GitHub-Api-Version", "2022-11-28")
|
|
114
|
+
try:
|
|
115
|
+
with urllib.request.urlopen(req) as resp:
|
|
116
|
+
return json.loads(resp.read().decode("utf-8"))
|
|
117
|
+
except urllib.error.HTTPError as e:
|
|
118
|
+
err_body = e.read().decode("utf-8")
|
|
119
|
+
print(f"ERROR: GitHub REST API {method} {path} failed ({e.code}): {err_body}")
|
|
120
|
+
sys.exit(1)
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
# --- PR Commands ---
|
|
124
|
+
|
|
125
|
+
def cmd_pr_get(args):
|
|
126
|
+
root = find_repo_root()
|
|
127
|
+
remote_url = get_remote_url(root)
|
|
128
|
+
owner_repo = parse_owner_repo(remote_url)
|
|
129
|
+
|
|
130
|
+
result = gh_cmd([
|
|
131
|
+
"pr", "view", str(args.pr_id),
|
|
132
|
+
"--repo", owner_repo,
|
|
133
|
+
"--json", "number,title,body,headRefName,baseRefName,author,state,headRefOid,url",
|
|
134
|
+
])
|
|
135
|
+
|
|
136
|
+
print(f"PR_ID: {result.get('number', args.pr_id)}")
|
|
137
|
+
print(f"TITLE: {result.get('title', '')}")
|
|
138
|
+
print(f"DESCRIPTION: {result.get('body', '')}")
|
|
139
|
+
print(f"SOURCE_BRANCH: {result.get('headRefName', '')}")
|
|
140
|
+
print(f"TARGET_BRANCH: {result.get('baseRefName', '')}")
|
|
141
|
+
author = result.get("author", {})
|
|
142
|
+
print(f"CREATOR: {author.get('login', '') if isinstance(author, dict) else str(author)}")
|
|
143
|
+
print(f"STATUS: {result.get('state', '')}")
|
|
144
|
+
print(f"REPO_ID: {owner_repo}")
|
|
145
|
+
print(f"REPO_NAME: {owner_repo.split('/')[-1]}")
|
|
146
|
+
print(f"HEAD_COMMIT: {result.get('headRefOid', '')}")
|
|
147
|
+
print(f"WORK_ITEMS: ") # GitHub has no native work item link (use PR body parsing if needed)
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def cmd_pr_diff(args):
|
|
151
|
+
root = find_repo_root()
|
|
152
|
+
remote_url = get_remote_url(root)
|
|
153
|
+
owner_repo = parse_owner_repo(remote_url)
|
|
154
|
+
|
|
155
|
+
result = gh_cmd([
|
|
156
|
+
"pr", "view", str(args.pr_id),
|
|
157
|
+
"--repo", owner_repo,
|
|
158
|
+
"--json", "headRefName,baseRefName,headRefOid",
|
|
159
|
+
])
|
|
160
|
+
|
|
161
|
+
source_branch = result.get("headRefName", "")
|
|
162
|
+
target_branch = result.get("baseRefName", "")
|
|
163
|
+
fetch_head = result.get("headRefOid", "")
|
|
164
|
+
|
|
165
|
+
print(f"SOURCE_BRANCH: {source_branch}")
|
|
166
|
+
print(f"TARGET_BRANCH: {target_branch}")
|
|
167
|
+
|
|
168
|
+
fetch_result = subprocess.run(
|
|
169
|
+
["git", "fetch", "origin", source_branch],
|
|
170
|
+
capture_output=True,
|
|
171
|
+
cwd=str(root),
|
|
172
|
+
)
|
|
173
|
+
if fetch_result.returncode != 0:
|
|
174
|
+
warn = (fetch_result.stderr or b"").decode("utf-8", errors="replace")
|
|
175
|
+
print(f"WARNING: git fetch failed: {warn}")
|
|
176
|
+
|
|
177
|
+
if not fetch_head:
|
|
178
|
+
head_result = subprocess.run(
|
|
179
|
+
["git", "rev-parse", "FETCH_HEAD"],
|
|
180
|
+
capture_output=True,
|
|
181
|
+
cwd=str(root),
|
|
182
|
+
)
|
|
183
|
+
fetch_head = head_result.stdout.decode("utf-8").strip() if head_result.returncode == 0 else ""
|
|
184
|
+
|
|
185
|
+
print(f"FETCH_HEAD: {fetch_head}")
|
|
186
|
+
|
|
187
|
+
diff_result = subprocess.run(
|
|
188
|
+
["git", "diff", "--name-status", f"origin/{target_branch}...FETCH_HEAD"],
|
|
189
|
+
capture_output=True,
|
|
190
|
+
cwd=str(root),
|
|
191
|
+
)
|
|
192
|
+
if diff_result.returncode == 0:
|
|
193
|
+
print("CHANGED_FILES:")
|
|
194
|
+
print(diff_result.stdout.decode("utf-8", errors="replace").strip())
|
|
195
|
+
else:
|
|
196
|
+
warn = (diff_result.stderr or b"").decode("utf-8", errors="replace")
|
|
197
|
+
print(f"WARNING: git diff failed: {warn}")
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def cmd_pr_comment(args):
|
|
201
|
+
root = find_repo_root()
|
|
202
|
+
remote_url = get_remote_url(root)
|
|
203
|
+
owner_repo = parse_owner_repo(remote_url)
|
|
204
|
+
|
|
205
|
+
tmp = tempfile.NamedTemporaryFile(mode="w", suffix=".md", delete=False, encoding="utf-8")
|
|
206
|
+
tmp.write(args.comment)
|
|
207
|
+
tmp.close()
|
|
208
|
+
try:
|
|
209
|
+
result = subprocess.run(
|
|
210
|
+
["gh", "pr", "comment", str(args.pr_id),
|
|
211
|
+
"--repo", owner_repo,
|
|
212
|
+
"--body-file", tmp.name],
|
|
213
|
+
capture_output=True,
|
|
214
|
+
)
|
|
215
|
+
if result.returncode != 0:
|
|
216
|
+
err = (result.stderr or b"").decode("utf-8", errors="replace")
|
|
217
|
+
print(f"ERROR: gh pr comment failed: {err}")
|
|
218
|
+
sys.exit(1)
|
|
219
|
+
print(f"Posted comment on PR #{args.pr_id}")
|
|
220
|
+
finally:
|
|
221
|
+
try:
|
|
222
|
+
os.unlink(tmp.name)
|
|
223
|
+
except OSError:
|
|
224
|
+
pass
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
def cmd_pr_inline(args):
|
|
228
|
+
root = find_repo_root()
|
|
229
|
+
remote_url = get_remote_url(root)
|
|
230
|
+
owner_repo = parse_owner_repo(remote_url)
|
|
231
|
+
token = get_github_token()
|
|
232
|
+
|
|
233
|
+
# Get HEAD commit for the PR (required by GitHub inline comment API)
|
|
234
|
+
result = gh_cmd([
|
|
235
|
+
"pr", "view", str(args.pr_id),
|
|
236
|
+
"--repo", owner_repo,
|
|
237
|
+
"--json", "headRefOid",
|
|
238
|
+
])
|
|
239
|
+
commit_id = result.get("headRefOid", "")
|
|
240
|
+
if not commit_id:
|
|
241
|
+
print("ERROR: Could not get PR head commit ID")
|
|
242
|
+
sys.exit(1)
|
|
243
|
+
|
|
244
|
+
file_path = args.file_path.replace("\\", "/").lstrip("/")
|
|
245
|
+
|
|
246
|
+
data = {
|
|
247
|
+
"body": args.comment,
|
|
248
|
+
"commit_id": commit_id,
|
|
249
|
+
"path": file_path,
|
|
250
|
+
"line": args.line,
|
|
251
|
+
"side": "RIGHT",
|
|
252
|
+
}
|
|
253
|
+
resp = github_rest("POST", f"/repos/{owner_repo}/pulls/{args.pr_id}/comments", data, token)
|
|
254
|
+
comment_id = resp.get("id", "")
|
|
255
|
+
print(f"Posted inline comment on {file_path}:{args.line} (comment #{comment_id})")
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
def cmd_pr_vote(args):
|
|
259
|
+
root = find_repo_root()
|
|
260
|
+
remote_url = get_remote_url(root)
|
|
261
|
+
owner_repo = parse_owner_repo(remote_url)
|
|
262
|
+
|
|
263
|
+
vote = args.vote.lower()
|
|
264
|
+
if vote == "approve":
|
|
265
|
+
gh_args = ["pr", "review", str(args.pr_id), "--repo", owner_repo, "--approve"]
|
|
266
|
+
gh_cmd(gh_args)
|
|
267
|
+
print(f"Approved PR #{args.pr_id}")
|
|
268
|
+
elif vote == "reject":
|
|
269
|
+
gh_args = [
|
|
270
|
+
"pr", "review", str(args.pr_id), "--repo", owner_repo,
|
|
271
|
+
"--request-changes",
|
|
272
|
+
"--body", "AI review found Critical or High severity issues. See inline comments for details.",
|
|
273
|
+
]
|
|
274
|
+
gh_cmd(gh_args)
|
|
275
|
+
print(f"Requested changes on PR #{args.pr_id}")
|
|
276
|
+
elif vote in ("wait-for-author", "wait"):
|
|
277
|
+
gh_args = [
|
|
278
|
+
"pr", "review", str(args.pr_id), "--repo", owner_repo,
|
|
279
|
+
"--comment",
|
|
280
|
+
"--body", "AI review: waiting for author to address review comments.",
|
|
281
|
+
]
|
|
282
|
+
gh_cmd(gh_args)
|
|
283
|
+
print(f"Posted 'waiting for author' comment on PR #{args.pr_id}")
|
|
284
|
+
elif vote == "reset":
|
|
285
|
+
print(f"INFO: GitHub does not support vote reset. No action taken for PR #{args.pr_id}")
|
|
286
|
+
else:
|
|
287
|
+
print(f"ERROR: Unknown vote '{vote}'. Use: approve|reject|wait-for-author|reset")
|
|
288
|
+
sys.exit(1)
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
# --- Main ---
|
|
292
|
+
|
|
293
|
+
def main():
|
|
294
|
+
parser = argparse.ArgumentParser(description="TAS GitHub PR Integration")
|
|
295
|
+
subparsers = parser.add_subparsers(dest="command", help="Command to run")
|
|
296
|
+
|
|
297
|
+
p = subparsers.add_parser("pr-get", help="Get PR metadata")
|
|
298
|
+
p.add_argument("pr_id", type=int, help="Pull Request number")
|
|
299
|
+
|
|
300
|
+
p = subparsers.add_parser("pr-diff", help="Fetch PR branch and list changed files")
|
|
301
|
+
p.add_argument("pr_id", type=int, help="Pull Request number")
|
|
302
|
+
|
|
303
|
+
p = subparsers.add_parser("pr-comment", help="Post summary comment on PR")
|
|
304
|
+
p.add_argument("pr_id", type=int, help="Pull Request number")
|
|
305
|
+
p.add_argument("--comment", required=True, help="Comment text (markdown supported)")
|
|
306
|
+
|
|
307
|
+
p = subparsers.add_parser("pr-inline", help="Post inline comment on PR diff")
|
|
308
|
+
p.add_argument("pr_id", type=int, help="Pull Request number")
|
|
309
|
+
p.add_argument("--file", dest="file_path", required=True, help="File path (e.g. src/api/users.ts)")
|
|
310
|
+
p.add_argument("--line", type=int, required=True, help="Line number")
|
|
311
|
+
p.add_argument("--comment", required=True, help="Inline comment text")
|
|
312
|
+
|
|
313
|
+
p = subparsers.add_parser("pr-vote", help="Set vote on PR")
|
|
314
|
+
p.add_argument("pr_id", type=int, help="Pull Request number")
|
|
315
|
+
p.add_argument("--vote", required=True,
|
|
316
|
+
choices=["approve", "reject", "wait-for-author", "reset"],
|
|
317
|
+
help="Vote to cast")
|
|
318
|
+
|
|
319
|
+
args = parser.parse_args()
|
|
320
|
+
if not args.command:
|
|
321
|
+
parser.print_help()
|
|
322
|
+
sys.exit(1)
|
|
323
|
+
|
|
324
|
+
if args.command == "pr-get":
|
|
325
|
+
cmd_pr_get(args)
|
|
326
|
+
elif args.command == "pr-diff":
|
|
327
|
+
cmd_pr_diff(args)
|
|
328
|
+
elif args.command == "pr-comment":
|
|
329
|
+
cmd_pr_comment(args)
|
|
330
|
+
elif args.command == "pr-inline":
|
|
331
|
+
cmd_pr_inline(args)
|
|
332
|
+
elif args.command == "pr-vote":
|
|
333
|
+
cmd_pr_vote(args)
|
|
334
|
+
else:
|
|
335
|
+
parser.print_help()
|
|
336
|
+
|
|
337
|
+
|
|
338
|
+
if __name__ == "__main__":
|
|
339
|
+
main()
|