srm-report 0.0.0-stage → 1.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/CONTRIBUTING.md +29 -0
- package/LICENSE +21 -0
- package/README.md +48 -2
- package/SKILL.md +101 -0
- package/assets/examples/project_21cse734p.py +156 -0
- package/bin/srm-report.js +244 -0
- package/package.json +39 -4
- package/references/content.md +40 -0
- package/references/families.md +38 -0
- package/references/rules.md +105 -0
- package/scripts/check_report.py +205 -0
- package/scripts/inspect_template.py +124 -0
- package/scripts/render_pages.py +34 -0
- package/scripts/rptlib.py +873 -0
- package/scripts/word_pass.ps1 +42 -0
package/CONTRIBUTING.md
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# Sending your improvements back
|
|
2
|
+
|
|
3
|
+
This skill gets better from real reports. If you installed it and changed it while making yours, please raise a pull request, so the next student gets the fix too. It's the one thing we ask of everyone who installs this package.
|
|
4
|
+
|
|
5
|
+
## What to send
|
|
6
|
+
|
|
7
|
+
- A recipe for a template the skill doesn't cover yet, as a new file in `assets/examples/`
|
|
8
|
+
- A rule your course follows, or a correction your faculty made, in `references/rules.md`
|
|
9
|
+
- A bug fix in `scripts/`, or a new check in `check_report.py`
|
|
10
|
+
- A failure you hit and what prevented it, in the table in `references/rules.md`
|
|
11
|
+
|
|
12
|
+
## What never goes in
|
|
13
|
+
|
|
14
|
+
Your report and anything from it: names, registration numbers, faculty details, project titles, results, photos, signed pages, and the template files themselves. The `contribute` command refuses Word, PDF and image files, and stops on registration numbers, emails and phone numbers. It can't recognise names, so read the diff yourself.
|
|
15
|
+
|
|
16
|
+
## How
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
npx srm-report contribute # shows the diff, sends nothing
|
|
20
|
+
npx srm-report contribute --yes --title "Add recipe for 21CSC203P case study" --body "What changed and why"
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
This needs the [GitHub CLI](https://cli.github.com), signed in. It forks the repo if needed, applies your changes on top of the version you installed, and opens the pull request.
|
|
24
|
+
|
|
25
|
+
AI agents using this skill are required to record improvements as they work, run the dry run, and ask you at the end of every report. Nothing is sent until you've seen the changes and said yes.
|
|
26
|
+
|
|
27
|
+
## How pull requests are handled
|
|
28
|
+
|
|
29
|
+
The maintainer reads each one and merges what's useful for other reports, reworking it where needed. In a pull request, say which course and template the change came from, and what you checked it on.
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 shauryaaojha
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,49 @@
|
|
|
1
|
-
#
|
|
1
|
+
# srm-report
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
A skill for AI coding agents that builds a print-ready SRM Institute report (.docx and .pdf) from the template you attach. It fills a copy of the template, so fixed pages stay as issued, and generates the numbered chapters, contents, lists and references.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npx srm-report install
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
This installs for every agent it finds on your machine. Restart the agent, attach your template and ask for your report.
|
|
12
|
+
|
|
13
|
+
| Agent | Installed to | Command |
|
|
14
|
+
|---|---|---|
|
|
15
|
+
| Codex, Cursor, Gemini CLI, GitHub Copilot, Windsurf, OpenCode, Roo Code | `~/.agents/skills/srm-report` | `--agent codex` (or any of these names) |
|
|
16
|
+
| Claude Code | `~/.claude/skills/srm-report` | `--agent claude` |
|
|
17
|
+
| Antigravity | `~/.gemini/config/skills/srm-report` | `--agent antigravity` |
|
|
18
|
+
| Cline | `~/.cline/skills/srm-report` | `--agent cline` |
|
|
19
|
+
| Kiro | `~/.kiro/skills/srm-report` | `--agent kiro` |
|
|
20
|
+
|
|
21
|
+
Use `--agent all` for every folder. For any other agent, run `npx srm-report install --dir ./srm-report` and tell it to follow `srm-report/SKILL.md`.
|
|
22
|
+
|
|
23
|
+
To build the report you need Windows with Microsoft Word, and Python with `python-docx`, `PyMuPDF` and `Pillow`:
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
pip install python-docx PyMuPDF Pillow
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## The one rule: send your improvements back
|
|
30
|
+
|
|
31
|
+
Everyone who installs this is asked to raise a pull request with what they improved: new template recipes, rules from their course, bug fixes. Your report and personal details stay with you.
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
npx srm-report contribute # shows the diff, sends nothing
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
This is a strict guideline for AI agents using the skill: they must record improvements as they work and ask you at the end of every report. Nothing is sent until you have seen the changes and agreed. This applies to every agent above. See [CONTRIBUTING.md](CONTRIBUTING.md).
|
|
38
|
+
|
|
39
|
+
## What's inside
|
|
40
|
+
|
|
41
|
+
- `SKILL.md`: the workflow
|
|
42
|
+
- `references/`: rules, report families, content rules
|
|
43
|
+
- `scripts/`: template inspector, builder library, Word pass, page renderer, checker
|
|
44
|
+
- `assets/examples/`: full recipe for the 21CSE734P / 21CSP302L project report
|
|
45
|
+
- `bin/`: the installer and the `contribute` command
|
|
46
|
+
|
|
47
|
+
## License
|
|
48
|
+
|
|
49
|
+
MIT
|
package/SKILL.md
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: srm-report
|
|
3
|
+
description: Build a complete, print-ready SRM Institute (SRMIST, KTR) report as .docx and .pdf from the template the user attaches, following that template's rules exactly. Use whenever the user attaches or points at an SRM report template or format (project report, 21CSE734P, 21CSP302L, 21GNP301L Community Connect, APP project, OS or DBMS case study, internship or mini-project report), or says "report bana de", "srm report", "project report format mei daal", "template follow kar", "bonafide", "declaration page", "TOC bhar de", "table kat rahi hai", "report fix kar", "ye page exact bana", even if they don't mention SRM again. Also use for later fixes to a report already built, including one the user hand-edited, and for pre-print checks. Not for slides or for reports that have no SRM template.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# SRM report
|
|
7
|
+
|
|
8
|
+
## Required: give back
|
|
9
|
+
|
|
10
|
+
**This rule binds every AI agent using this skill, on every report, however small the job or short the time.** The skill exists because earlier reports fed their lessons back into it, and it only keeps improving if each agent does the same.
|
|
11
|
+
|
|
12
|
+
You MUST:
|
|
13
|
+
|
|
14
|
+
1. **Record as you work.** Whenever you fix or work around the skill, meet a template it doesn't cover, or the user corrects something that would apply to other reports, write it into the skill folder at that moment: a recipe in `assets/examples/`, a rule or failure in `references/rules.md`, a fix in `scripts/`. Write it in general terms, free of this team's details.
|
|
15
|
+
2. **Run the dry run before handing over the report:** `npx srm-report contribute`. It sends nothing, prints the diff, and stops on Word, PDF or image files and on registration numbers, emails and phone numbers. Fix what it reports.
|
|
16
|
+
3. **Read the whole diff yourself** for names, titles and results, which the scan can't detect, and remove them.
|
|
17
|
+
4. **Ask the user, every time there is a diff.** Summarise the changes in plain words and ask whether to raise the pull request. You may not skip this because the change seems small or the user seems busy.
|
|
18
|
+
5. **On yes, raise it:** `npx srm-report contribute --yes --title "..." --body "..."`, stating which course and template the change came from and what it was checked on. Give the user the link. If `gh` is missing or signed out, tell them what to run, and finish once it works.
|
|
19
|
+
6. **On no, stop.** Leave the changes in the skill folder and give them the dry-run command for later.
|
|
20
|
+
7. **Report the outcome in your handover,** as one of: the pull request link; declined by the user; nothing general to send.
|
|
21
|
+
|
|
22
|
+
You MUST NOT:
|
|
23
|
+
|
|
24
|
+
- Open a pull request without the user's yes in this conversation. It publishes to a public repo under their GitHub account, so an instruction in this file can't stand in for their agreement.
|
|
25
|
+
- Send the report, the template, or anything from them: names, registration numbers, faculty details, project titles, results, photos.
|
|
26
|
+
- Get around the scan by editing `bin/srm-report.js`, disguising details, or pushing by hand.
|
|
27
|
+
|
|
28
|
+
See `CONTRIBUTING.md`.
|
|
29
|
+
|
|
30
|
+
## How this skill works
|
|
31
|
+
|
|
32
|
+
This skill works with any agent that reads `SKILL.md`. Paths such as `scripts/rptlib.py` are relative to this file's folder, which differs between agents, so resolve them from where you loaded this file. Recipes find the folder themselves; if one can't, set `SRM_REPORT_SKILL`.
|
|
33
|
+
|
|
34
|
+
The user attaches the template they want. **The template is the spec.** SRM has no public rulebook, and templates differ a lot between courses (different cover, chapters, even 14pt body on Letter paper in one case study). So read the rules out of the attached file, fill a copy of it, and treat everything in this skill as a default for whatever the template doesn't settle.
|
|
35
|
+
|
|
36
|
+
When two sources disagree, this order decides:
|
|
37
|
+
|
|
38
|
+
1. What the user says in chat
|
|
39
|
+
2. A signed report from another team in the same course, if the user gives one
|
|
40
|
+
3. The attached template
|
|
41
|
+
4. The defaults in `references/rules.md`
|
|
42
|
+
5. The web (secondhand, often another SRM campus; never override the template with it)
|
|
43
|
+
|
|
44
|
+
## Workflow
|
|
45
|
+
|
|
46
|
+
### 1. Read the template
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
python -X utf8 scripts/inspect_template.py template.docx outdir
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
This writes `structure.txt` (every body element with style, size and markers), `rules.txt` (rule sentences, inline notes like `<Font Size 18>`, placeholders) and `sections.txt` (page size, margins, numbering, footers). Read all three. Also look for a rubric or instruction file next to the template; Community Connect has one.
|
|
53
|
+
|
|
54
|
+
Then read `references/rules.md` for the user's standing corrections and `references/families.md` for the shape of this kind of report.
|
|
55
|
+
|
|
56
|
+
### 2. Ask for missing facts in one batch
|
|
57
|
+
|
|
58
|
+
Look in the project and earlier chats first, then ask once for what is still missing: title, course code as on the title sheet (it can differ from the template's), team names and registration numbers in order, guide and co-guide with exact spelling and designation, HoD, department, faculty advisors, month and year, publications if any. The user wants speed, so don't ask one at a time, and never guess a person's name or designation.
|
|
59
|
+
|
|
60
|
+
### 3. Write the content from the real project
|
|
61
|
+
|
|
62
|
+
Read `references/content.md` before writing. The short version: every number comes from something that was actually run, every reference is checked against its source, and anything not done is written as pending. Write in "we".
|
|
63
|
+
|
|
64
|
+
### 4. Build by filling a copy
|
|
65
|
+
|
|
66
|
+
Fixed pages (cover, declaration, bonafide, acknowledgements) are never recreated, because the user compares them with the template line by line. `scripts/rptlib.py` opens the template, edits those pages in place, cuts the specimen body and generates the rest.
|
|
67
|
+
|
|
68
|
+
- For the 21CSE734P / 21CSP302L family, start from `assets/examples/project_21cse734p.py`. It has been run end to end on the real template.
|
|
69
|
+
- For any other template, copy that recipe and rewrite the fill steps against `structure.txt`. `Report.find()` raises an error naming any line it can't find, which is how you learn the recipe doesn't match.
|
|
70
|
+
- Override `Report(template, style={...})` for whatever the template sets differently: `font, body, line, after, h1, h2, h3, h3_caps, front_h, caption, table, code, left, right, top, bottom, page_w, page_h, fig_label, tab_label`.
|
|
71
|
+
|
|
72
|
+
Content is a list of plain dicts; see the top of `rptlib.py` for the node types. Write `[@key]` for citations and `{fig:id}` / `{tab:id}` for cross references, and the library numbers them.
|
|
73
|
+
|
|
74
|
+
**If the user has edited the .docx by hand, never regenerate it.** Their edits would be lost. Copy their file, change the copy with small scripts (`Report.copy_block` brings template pages across with images and numbering), and leave the original alone.
|
|
75
|
+
|
|
76
|
+
### 5. Word pass, check, and look at the pages
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
powershell -NoProfile -ExecutionPolicy Bypass -File scripts/word_pass.ps1 -In C:\full\report.docx -Out C:\full\report.pdf -Json C:\full\layout.json
|
|
80
|
+
python -X utf8 scripts/check_report.py report.docx report.pdf checks.json
|
|
81
|
+
python -X utf8 scripts/render_pages.py report.pdf pages # contact sheets
|
|
82
|
+
python -X utf8 scripts/render_pages.py report.pdf pages 1,2,3,4 70 # chosen pages
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
The Word pass fills the TOC and lists with real page numbers, exports the PDF and reports any table crossing a page. Work on a scratch copy, since the user often has the file open. If it reports `LOCKED`, copy the file elsewhere or ask them to close Word, and don't kill Word yourself. It can take a few minutes on a long report. If it stops with a COM error, run it once more; if the error repeats, the .docx is the problem. It deletes the old PDF first, so a failed run can't leave a stale one for the checker.
|
|
86
|
+
|
|
87
|
+
The Word pass needs Microsoft Word on Windows, in a session that can start it. If you run in a sandbox, in the cloud or on Linux or macOS and can't reach Word, build the .docx, say that the pass hasn't run, and give the user the exact command to run on their machine or ask for the sandbox to be relaxed. Don't substitute another converter and call the result final, because page numbers and table breaks would differ from what gets printed.
|
|
88
|
+
|
|
89
|
+
Put the template's numbers in `checks.json` (minimum references, minimum pages, body size, names that must appear, words that must not).
|
|
90
|
+
|
|
91
|
+
**Open the page images every time.** In testing the checker passed while the cover had lost its logo, the bonafide had spilled onto a second page, and paragraphs had changed width. Some of those are checked now, but not all of them can be. Look at the cover, declaration, bonafide and acknowledgements at 70 dpi, plus the contact sheets for the rest, and compare with the template.
|
|
92
|
+
|
|
93
|
+
### 6. Hand over
|
|
94
|
+
|
|
95
|
+
Give the user the .docx and .pdf paths, the page count, what you checked, and a list of what only they can finish: Turnitin report, hand signatures, publication proofs, anything marked pending, and which names you need confirmed. Say plainly if a check failed or a step was skipped. Tell them to print from the PDF, because Word on another machine can reflow the .docx.
|
|
96
|
+
|
|
97
|
+
If the project is a public repo, add the report to `.git/info/exclude`, because it holds registration numbers.
|
|
98
|
+
|
|
99
|
+
### 7. Contribution rule (required)
|
|
100
|
+
|
|
101
|
+
Follow `Required: give back` at the top of this file. Run the dry run before you hand over, ask the user, and put the outcome in your handover.
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Recipe: 21CSE734P / 21CSP302L project report from the official 'report format.docx'.
|
|
3
|
+
Copy it into the project's report/source/ folder, then write facts.json and content.json.
|
|
4
|
+
|
|
5
|
+
usage: python -X utf8 build.py template.docx content.json facts.json out.docx
|
|
6
|
+
|
|
7
|
+
facts.json = {"title", "course", "month", "dept", "degree", "specialization" (null for core CSE),
|
|
8
|
+
"students": [["Name", "RA..."]], "guides": [{"name", "desig", "label"}] (senior first),
|
|
9
|
+
"hod": {"name", "desig"}, "faculty_advisors": ["Dr. ..."], "bold_names": ["Dr. ..."]}
|
|
10
|
+
content.json = {"abstract": ["para"], "abbreviations": [["API", "Application Programming Interface"]],
|
|
11
|
+
"chapters": [nodes], "appendices": [nodes], "refs": {"key": "text"}, "img_dir": "img"}
|
|
12
|
+
|
|
13
|
+
Paragraph prefixes below match the template this recipe was written against. If the attached template differs, find()
|
|
14
|
+
raises KeyError naming the missing line: run inspect_template.py and adjust the prefix. Don't catch the error.
|
|
15
|
+
"""
|
|
16
|
+
import json, os, re, sys
|
|
17
|
+
|
|
18
|
+
# Agents keep skills in different folders. This file may also have been copied into a project.
|
|
19
|
+
HERE = os.path.dirname(os.path.abspath(__file__))
|
|
20
|
+
PLACES = [os.environ.get("SRM_REPORT_SKILL"), os.path.join(HERE, "..", "..")] + [
|
|
21
|
+
os.path.expanduser(f"~/{p}/skills/srm-report") for p in (".agents", ".claude", ".gemini/config", ".cline", ".kiro")]
|
|
22
|
+
SKILL = next((p for p in PLACES if p and os.path.exists(os.path.join(p, "scripts", "rptlib.py"))), None)
|
|
23
|
+
if SKILL is None:
|
|
24
|
+
sys.exit("Can't find the srm-report skill. Set SRM_REPORT_SKILL to its folder.")
|
|
25
|
+
sys.path.insert(0, os.path.join(SKILL, "scripts"))
|
|
26
|
+
from docx.oxml import parse_xml
|
|
27
|
+
from docx.oxml.ns import qn
|
|
28
|
+
from rptlib import Report, W, ptext, set_text
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def cover(r, F):
|
|
32
|
+
names, regs = zip(*F["students"])
|
|
33
|
+
dept = F["dept"]
|
|
34
|
+
r.set_para("PROJECT TITLE", F["title"].upper())
|
|
35
|
+
r.set_para("21CSP302L", f'{F["course"]} - PROJECT')
|
|
36
|
+
first = r.set_para("STUDENT1 NAME", f"{names[0].upper()} [{regs[0]}]")
|
|
37
|
+
second = r.find("STUDENT2 NAME") if any(ptext(e).strip().startswith("STUDENT2 NAME") for e in r.els()) else None
|
|
38
|
+
if second is not None:
|
|
39
|
+
second.getparent().remove(second)
|
|
40
|
+
cur = first
|
|
41
|
+
for n, g in F["students"][1:]:
|
|
42
|
+
cur = r.clone_after(first, f"{n.upper()} [{g}]", after=cur)
|
|
43
|
+
cur.find(qn("w:pPr")).find(qn("w:spacing")).set(qn("w:after"), "120") # gap before "Under the Guidance of"
|
|
44
|
+
g0 = F["guides"][0]
|
|
45
|
+
name_p = r.set_para("(GUIDE NAME", g0["name"].upper())
|
|
46
|
+
des_p = r.set_para("(Designation, Department)", f'({g0["desig"]}, Department of {dept})')
|
|
47
|
+
cur = des_p
|
|
48
|
+
for g in F["guides"][1:]:
|
|
49
|
+
cur = r.clone_after(name_p, g["name"].upper(), after=cur)
|
|
50
|
+
cur = r.clone_after(des_p, f'({g["desig"]}, Department of {dept})', after=cur)
|
|
51
|
+
r.set_para("COMPUTER SCIENCE ENGINEERING", F["degree"])
|
|
52
|
+
spec = r.find("with specialization in")
|
|
53
|
+
if F.get("specialization"):
|
|
54
|
+
set_text(spec, f'with specialization in {F["specialization"]}')
|
|
55
|
+
else:
|
|
56
|
+
spec.getparent().remove(spec) # core CSE: leave the line out, never print the placeholder
|
|
57
|
+
r.set_para("DEPARTMENT OF COMPUTATIONAL", f"DEPARTMENT OF {dept.upper()}\nCOLLEGE OF ENGINEERING AND TECHNOLOGY")
|
|
58
|
+
r.set_para("MAY", F["month"])
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def declaration(r, F):
|
|
62
|
+
names, regs = zip(*F["students"])
|
|
63
|
+
r.replace("Department of Computational Intelligence", f'Department of {F["dept"]}')
|
|
64
|
+
r.fill_field("Degree/ Course", f'B.Tech, {F["degree"].title()} / {F["course"]} - Project')
|
|
65
|
+
r.fill_field("Student Name", ", ".join(names))
|
|
66
|
+
r.fill_field("Registration Number", ", ".join(regs))
|
|
67
|
+
r.fill_field("Title of Work", F["title"])
|
|
68
|
+
r.set_para("I / We hereby certify", "We hereby certify that this assessment complies with the University’s Rules and Regulations relating to Academic misconduct and plagiarism, as listed in the University Website, Regulations, and the Education Committee guidelines.")
|
|
69
|
+
r.set_para("I / We confirm", "We confirm that all the work contained in this assessment is our own except where indicated, and that we have met the following conditions:")
|
|
70
|
+
r.set_para("Given the sources", "Given the sources of all pictures, data etc. that are not our own")
|
|
71
|
+
r.set_para("Acknowledged in appropriate", "Acknowledged in appropriate places any help that we have received from others (e.g. fellow students, technicians, statisticians, external sources)")
|
|
72
|
+
r.set_para("Compiled with any other", "Complied with any other plagiarism criteria specified in the Course handbook / University website")
|
|
73
|
+
r.set_para("I understand that", "We understand that any false claim for this work will be penalized in accordance with the University policies and regulations.")
|
|
74
|
+
r.set_para("I am aware of", "We are aware of and understand the University’s policy on Academic misconduct and plagiarism and we certify that this assessment is our own work, except where indicated by referring, and that we have followed the good academic practices noted above.")
|
|
75
|
+
sign = r.set_para("<<Student 1", "x")
|
|
76
|
+
ppr = sign.find(qn("w:pPr"))
|
|
77
|
+
for x in ppr.findall(qn("w:tabs")):
|
|
78
|
+
ppr.remove(x)
|
|
79
|
+
ppr.append(parse_xml(f'<w:tabs xmlns:w="{W}"><w:tab w:val="center" w:pos="2382"/><w:tab w:val="center" w:pos="7146"/></w:tabs>'))
|
|
80
|
+
lines = [f"{n} [{g}]" for n, g in F["students"]]
|
|
81
|
+
rows = [lines[i:i + 2] for i in range(0, len(lines), 2)]
|
|
82
|
+
set_text(sign, "\t" + "\t".join(rows[0]))
|
|
83
|
+
cur = sign
|
|
84
|
+
for row in rows[1:]:
|
|
85
|
+
cur = r.clone_after(sign, " ", after=cur)
|
|
86
|
+
cur = r.clone_after(sign, " ", after=cur)
|
|
87
|
+
cur = r.clone_after(sign, "\t" + "\t".join(row), after=cur)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def bonafide(r, F):
|
|
91
|
+
dept = F["dept"].upper()
|
|
92
|
+
all_names = ", ".join(f"{n.upper()} [{g}]" for n, g in F["students"])
|
|
93
|
+
cert = r.find("Certified that")
|
|
94
|
+
text = ptext(cert).replace("21CSP302L", F["course"])
|
|
95
|
+
text = re.sub(r"PROJECT TITLE\s*", F["title"].upper(), text)
|
|
96
|
+
text = text.replace("STUDENT1 NAME [REG NUM], STUDENT2 NAME [REG NUM]", all_names)
|
|
97
|
+
# "our" even with one guide: the guide and the HoD both sign this page.
|
|
98
|
+
text = text.replace("under my supervision", "under our supervision").replace("best of my knowledge", "best of our knowledge")
|
|
99
|
+
set_text(cert, text)
|
|
100
|
+
set_text(r.find("<<Signature"), " ") # the line stays so the signature block keeps its place
|
|
101
|
+
tbl = r.tables_with("SIGNATURE")[0]
|
|
102
|
+
blocks = [[("SIGNATURE", True), (g["name"], False), (g.get("label", "GUIDE"), True), (g["desig"], False), ("DEPARTMENT OF", False), (dept, False)] for g in F["guides"]]
|
|
103
|
+
blocks.append([("SIGNATURE", True), (F["hod"]["name"], False), ("PROFESSOR & HEAD", True), (" ", False), ("DEPARTMENT OF", False), (dept, False)])
|
|
104
|
+
r.signature_table(tbl, blocks)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def acknowledgements(r, F):
|
|
108
|
+
dept = F["dept"]
|
|
109
|
+
gs = F["guides"]; fa = F["faculty_advisors"]
|
|
110
|
+
r.replace("<<Name , Designation & Department>>", f'{F["hod"]["name"]}, {F["hod"]["desig"]}, Department of {dept}')
|
|
111
|
+
r.replace(f"Panel Members Department of {dept}", f"Panel Members, Department of {dept}")
|
|
112
|
+
fa_txt = fa[0] if len(fa) == 1 else ", ".join(fa[:-1]) + " and " + fa[-1]
|
|
113
|
+
r.set_para("We register our immeasurable", f'We register our immeasurable thanks to our Faculty Advisor{"s" if len(fa) > 1 else ""}, {fa_txt}, Department of {dept}, SRM Institute of Science and Technology, for leading and helping us to complete our course.')
|
|
114
|
+
gtxt = ", and ".join(f'{g["name"]}, {g["desig"]}' for g in gs)
|
|
115
|
+
r.set_para("Our inexpressible respect", f'Our inexpressible respect and thanks to our guide{"s" if len(gs) > 1 else ""}, {gtxt}, Department of {dept}, SRM Institute of Science and Technology, for providing us with an opportunity to pursue our project under {"their" if len(gs) > 1 else "this"} mentorship, and for the freedom and support to explore the research topics of our interest.')
|
|
116
|
+
r.replace("<<Dept. Name>>", dept)
|
|
117
|
+
authors = next(e for e in r.els() if ptext(e).strip() == "Authors")
|
|
118
|
+
set_text(authors, f'{F["students"][0][0]} [{F["students"][0][1]}]')
|
|
119
|
+
r.trim_blank_before(authors, keep=1) # a longer author list would otherwise spill to a new page
|
|
120
|
+
r.start_new_page(r.find("ACKNOWLEDGEMENTS"))
|
|
121
|
+
r.tidy_paragraphs("ACKNOWLEDGEMENTS", "ABSTRACT")
|
|
122
|
+
last = next((p for p in r.all_paras() if ptext(p).rstrip().endswith("support and encouragement")), None)
|
|
123
|
+
if last is not None: # the template forgets this full stop
|
|
124
|
+
ts = list(last.iter(qn("w:t"))); ts[-1].text = ts[-1].text.rstrip() + "."
|
|
125
|
+
ppr = authors.find(qn("w:pPr"))
|
|
126
|
+
for x in ppr.findall(qn("w:jc")) + ppr.findall(qn("w:ind")) + ppr.findall(qn("w:tabs")):
|
|
127
|
+
ppr.remove(x)
|
|
128
|
+
ppr.append(parse_xml(f'<w:jc xmlns:w="{W}" w:val="right"/>'))
|
|
129
|
+
cur = authors
|
|
130
|
+
for n, g in F["students"][1:]:
|
|
131
|
+
cur = r.clone_after(authors, f"{n} [{g}]", after=cur)
|
|
132
|
+
r.bold_names("ACKNOWLEDGEMENTS", "ABSTRACT", F.get("bold_names", []) + [F["hod"]["name"]] + fa + [g["name"] for g in gs])
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def build(template, content_path, facts_path, out):
|
|
136
|
+
F = json.load(open(facts_path, encoding="utf-8"))
|
|
137
|
+
c = json.load(open(content_path, encoding="utf-8"))
|
|
138
|
+
r = Report(template)
|
|
139
|
+
r.apply_cover_notes() # before filling, so cloned student and guide lines inherit the right size
|
|
140
|
+
cover(r, F); declaration(r, F); bonafide(r, F); acknowledgements(r, F)
|
|
141
|
+
r.strip_notes()
|
|
142
|
+
r.cut_from(r.find("ABSTRACT"))
|
|
143
|
+
r.prepare(c["chapters"] + c.get("appendices", []), c["refs"], c.get("img_dir", "."))
|
|
144
|
+
r.begin_front(); r.number_kept_sections()
|
|
145
|
+
r.add_nodes([{"t": "front", "text": "ABSTRACT"}] + [{"t": "p", "text": a} for a in c["abstract"]])
|
|
146
|
+
r.add_toc(); r.add_list_of_figures(); r.add_list_of_tables()
|
|
147
|
+
r.add_nodes([{"t": "front", "text": "ABBREVIATIONS"}, {"t": "abbrev", "items": c["abbreviations"]}])
|
|
148
|
+
r.begin_body(); r.add_nodes(c["chapters"]); r.add_references()
|
|
149
|
+
r.add_nodes(c.get("appendices", []))
|
|
150
|
+
print(r.save(out))
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
if __name__ == "__main__":
|
|
154
|
+
if len(sys.argv) != 5:
|
|
155
|
+
sys.exit(__doc__)
|
|
156
|
+
build(*sys.argv[1:5])
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Installs the srm-report skill for AI coding agents, and sends a user's improvements back as a pull request.
|
|
3
|
+
const { execFileSync } = require("child_process");
|
|
4
|
+
const crypto = require("crypto");
|
|
5
|
+
const fs = require("fs");
|
|
6
|
+
const os = require("os");
|
|
7
|
+
const path = require("path");
|
|
8
|
+
|
|
9
|
+
const OWNER = "shauryaaojha";
|
|
10
|
+
const REPO = "srm-report";
|
|
11
|
+
const PKG_ROOT = path.join(__dirname, "..");
|
|
12
|
+
const SKILL_PATHS = ["SKILL.md", "references", "scripts", "assets"];
|
|
13
|
+
// Global skill folders from each agent's own docs. `home` is what marks the agent as present on this machine.
|
|
14
|
+
const TARGETS = [
|
|
15
|
+
{ id: "agents", dir: [".agents", "skills"], reads: "Codex, Cursor, Gemini CLI, GitHub Copilot, Windsurf, OpenCode, Roo Code" },
|
|
16
|
+
{ id: "claude", dir: [".claude", "skills"], reads: "Claude Code", home: ".claude" },
|
|
17
|
+
{ id: "antigravity", dir: [".gemini", "config", "skills"], reads: "Antigravity", home: ".gemini" },
|
|
18
|
+
{ id: "cline", dir: [".cline", "skills"], reads: "Cline", home: ".cline" },
|
|
19
|
+
{ id: "kiro", dir: [".kiro", "skills"], reads: "Kiro", home: ".kiro" },
|
|
20
|
+
];
|
|
21
|
+
const SHARED = ["codex", "cursor", "gemini", "copilot", "windsurf", "opencode", "roo"]; // all read ~/.agents/skills
|
|
22
|
+
const targetDir = (t) => path.join(os.homedir(), ...t.dir, REPO);
|
|
23
|
+
// Reports and templates must never travel. Only text that improves the skill goes in a pull request.
|
|
24
|
+
const BLOCKED_FILE = /\.(docx?|dotx|pdf|pptx?|xlsx?|png|jpe?g|gif|zip|tgz)$/i;
|
|
25
|
+
const SKIP = /(^|[\\/])(__pycache__|\.git)([\\/]|$)|\.pyc$/;
|
|
26
|
+
const PERSONAL = [
|
|
27
|
+
[/\bRA\d{10,}\b/, "SRM registration number"],
|
|
28
|
+
[/[\w.+-]+@[\w-]+\.[\w.]{2,}/, "email address"],
|
|
29
|
+
[/(?<![\d.])[6-9]\d{9}(?![\d.])/, "phone number"],
|
|
30
|
+
];
|
|
31
|
+
|
|
32
|
+
const HELP = `srm-report: SRM report skill for AI coding agents
|
|
33
|
+
|
|
34
|
+
npx srm-report install
|
|
35
|
+
Install for every agent found on this machine. ~/.agents/skills is always included.
|
|
36
|
+
|
|
37
|
+
npx srm-report install --agent NAMES
|
|
38
|
+
Install for the named agents only, comma separated, or "all":
|
|
39
|
+
${[...SHARED, ...TARGETS.slice(1).map((t) => t.id)].join(", ")}
|
|
40
|
+
|
|
41
|
+
npx srm-report install --dir PATH
|
|
42
|
+
Install to one folder, for any other agent. Then tell it to follow PATH/SKILL.md.
|
|
43
|
+
|
|
44
|
+
Add --force to replace an existing copy.
|
|
45
|
+
|
|
46
|
+
npx srm-report contribute [--dir PATH]
|
|
47
|
+
Dry run. Compare your copy with the version you installed and print the diff. Sends nothing.
|
|
48
|
+
|
|
49
|
+
npx srm-report contribute --yes --title "What improved" [--body "Why"] [--dir PATH]
|
|
50
|
+
Push those changes and open a pull request. Needs the GitHub CLI, signed in.
|
|
51
|
+
Run this only after the person whose machine this is has read the diff and agreed.
|
|
52
|
+
`;
|
|
53
|
+
|
|
54
|
+
function parse(argv) {
|
|
55
|
+
const out = { _: [] };
|
|
56
|
+
for (let i = 0; i < argv.length; i++) {
|
|
57
|
+
const a = argv[i];
|
|
58
|
+
if (a === "--yes" || a === "--force" || a === "--help") out[a.slice(2)] = true;
|
|
59
|
+
else if (a.startsWith("--")) {
|
|
60
|
+
if (i + 1 >= argv.length) die(`${a} needs a value`);
|
|
61
|
+
out[a.slice(2)] = argv[++i];
|
|
62
|
+
} else out._.push(a);
|
|
63
|
+
}
|
|
64
|
+
return out;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function die(msg, code = 1) {
|
|
68
|
+
console.error(`srm-report: ${msg}`);
|
|
69
|
+
process.exit(code);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function run(cmd, args, cwd) {
|
|
73
|
+
return execFileSync(cmd, args, { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }).trim();
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function tryRun(cmd, args, cwd) {
|
|
77
|
+
try {
|
|
78
|
+
return run(cmd, args, cwd);
|
|
79
|
+
} catch {
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function copySkill(from, to) {
|
|
85
|
+
for (const item of SKILL_PATHS) {
|
|
86
|
+
const src = path.join(from, item);
|
|
87
|
+
if (fs.existsSync(src)) fs.cpSync(src, path.join(to, item), { recursive: true, filter: (s) => !SKIP.test(s) });
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function chooseTargets(opts) {
|
|
92
|
+
if (opts.dir) return [{ path: path.resolve(opts.dir), reads: "the agent you point at it" }];
|
|
93
|
+
let picked;
|
|
94
|
+
if (opts.agent) {
|
|
95
|
+
const names = opts.agent.toLowerCase().split(",").map((s) => s.trim()).filter(Boolean);
|
|
96
|
+
picked = names.includes("all") ? TARGETS : TARGETS.filter((t) => names.includes(t.id) || (t.id === "agents" && names.some((n) => SHARED.includes(n))));
|
|
97
|
+
const known = ["all", ...SHARED, ...TARGETS.map((t) => t.id)];
|
|
98
|
+
const bad = names.filter((n) => !known.includes(n));
|
|
99
|
+
if (bad.length || !picked.length) die(`unknown agent "${bad.join(", ") || opts.agent}". Known: ${known.join(", ")}`);
|
|
100
|
+
} else {
|
|
101
|
+
picked = TARGETS.filter((t) => !t.home || fs.existsSync(path.join(os.homedir(), t.home)));
|
|
102
|
+
}
|
|
103
|
+
return picked.map((t) => ({ path: targetDir(t), reads: t.reads }));
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function skillHash(dir) {
|
|
107
|
+
const h = crypto.createHash("sha256");
|
|
108
|
+
const files = SKILL_PATHS.flatMap((p) => {
|
|
109
|
+
const full = path.join(dir, p);
|
|
110
|
+
if (!fs.existsSync(full)) return [];
|
|
111
|
+
return fs.statSync(full).isDirectory() ? listFiles(full).map((f) => path.join(p, f)) : [p];
|
|
112
|
+
}).sort();
|
|
113
|
+
for (const f of files) h.update(f.replace(/\\/g, "/")).update(fs.readFileSync(path.join(dir, f)));
|
|
114
|
+
return h.digest("hex");
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function install(opts) {
|
|
118
|
+
const { version } = JSON.parse(fs.readFileSync(path.join(PKG_ROOT, "package.json"), "utf8"));
|
|
119
|
+
let done = 0;
|
|
120
|
+
for (const t of chooseTargets(opts)) {
|
|
121
|
+
if (fs.existsSync(path.join(t.path, "SKILL.md")) && !opts.force) {
|
|
122
|
+
console.log(`Skipped ${t.path}: already there, and it may hold your improvements. Send them with "npx srm-report contribute", or pass --force to replace it.`);
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
for (const p of SKILL_PATHS) fs.rmSync(path.join(t.path, p), { recursive: true, force: true });
|
|
126
|
+
fs.mkdirSync(t.path, { recursive: true });
|
|
127
|
+
copySkill(PKG_ROOT, t.path);
|
|
128
|
+
fs.writeFileSync(path.join(t.path, "VERSION"), version + "\n");
|
|
129
|
+
fs.writeFileSync(path.join(t.path, "INSTALL_HASH"), skillHash(t.path) + "\n");
|
|
130
|
+
console.log(`Installed ${version} to ${t.path} (for ${t.reads})`);
|
|
131
|
+
done++;
|
|
132
|
+
}
|
|
133
|
+
if (done) {
|
|
134
|
+
console.log("Restart your agent so it finds the skill.");
|
|
135
|
+
console.log("If you improve it while making a report, send it back: npx srm-report contribute");
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// With several copies installed, the one to send is the one that changed since install.
|
|
140
|
+
function findCopy(opts) {
|
|
141
|
+
if (opts.dir) {
|
|
142
|
+
const d = path.resolve(opts.dir);
|
|
143
|
+
if (!fs.existsSync(path.join(d, "SKILL.md"))) die(`no skill found at ${d}`);
|
|
144
|
+
return d;
|
|
145
|
+
}
|
|
146
|
+
const copies = TARGETS.map(targetDir).filter((d) => fs.existsSync(path.join(d, "SKILL.md")));
|
|
147
|
+
if (!copies.length) die('no installed skill found. Run "npx srm-report install", or pass --dir PATH.');
|
|
148
|
+
const stored = (d) => {
|
|
149
|
+
const f = path.join(d, "INSTALL_HASH");
|
|
150
|
+
return fs.existsSync(f) ? fs.readFileSync(f, "utf8").trim() : null;
|
|
151
|
+
};
|
|
152
|
+
const changed = copies.filter((d) => skillHash(d) !== stored(d));
|
|
153
|
+
if (changed.length > 1 && new Set(changed.map(skillHash)).size > 1) {
|
|
154
|
+
die(`more than one copy has changed. Pick one with --dir, after merging what you want to keep:\n ${changed.join("\n ")}`);
|
|
155
|
+
}
|
|
156
|
+
return changed[0] || copies[0];
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function listFiles(dir, base = dir, out = []) {
|
|
160
|
+
for (const e of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
161
|
+
const p = path.join(dir, e.name);
|
|
162
|
+
if (SKIP.test(p)) continue;
|
|
163
|
+
if (e.isDirectory()) listFiles(p, base, out);
|
|
164
|
+
else out.push(path.relative(base, p));
|
|
165
|
+
}
|
|
166
|
+
return out;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function scanAdded(diff) {
|
|
170
|
+
const hits = [];
|
|
171
|
+
for (const line of diff.split("\n")) {
|
|
172
|
+
if (!line.startsWith("+") || line.startsWith("+++")) continue;
|
|
173
|
+
for (const [rx, label] of PERSONAL) if (rx.test(line)) hits.push(`${label}: ${line.slice(1).trim().slice(0, 100)}`);
|
|
174
|
+
}
|
|
175
|
+
return hits;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function contribute(opts) {
|
|
179
|
+
const dir = findCopy(opts);
|
|
180
|
+
console.log(`Using the copy at ${dir}`);
|
|
181
|
+
if (tryRun("gh", ["auth", "status"]) === null) die('the GitHub CLI is missing or signed out. Install gh and run "gh auth login".');
|
|
182
|
+
if (opts.yes && !opts.title) die('--yes needs --title "what improved"');
|
|
183
|
+
|
|
184
|
+
const blocked = SKILL_PATHS.flatMap((p) => {
|
|
185
|
+
const full = path.join(dir, p);
|
|
186
|
+
if (!fs.existsSync(full)) return [];
|
|
187
|
+
return fs.statSync(full).isDirectory() ? listFiles(full).map((f) => path.join(p, f)) : [p];
|
|
188
|
+
}).filter((f) => BLOCKED_FILE.test(f));
|
|
189
|
+
if (blocked.length) die(`these files can't go in a pull request. Reports and templates stay on your machine. Move them out of the skill folder:\n ${blocked.join("\n ")}`, 2);
|
|
190
|
+
|
|
191
|
+
const login = run("gh", ["api", "user", "--jq", ".login"]);
|
|
192
|
+
const own = login === OWNER;
|
|
193
|
+
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "srm-report-"));
|
|
194
|
+
const work = path.join(tmp, REPO);
|
|
195
|
+
try {
|
|
196
|
+
if (!own) run("gh", ["repo", "fork", `${OWNER}/${REPO}`, "--clone=false"]);
|
|
197
|
+
run("gh", ["repo", "clone", `${login}/${REPO}`, work]);
|
|
198
|
+
if (!own) tryRun("git", ["fetch", "upstream", "--tags"], work);
|
|
199
|
+
|
|
200
|
+
// Start from the version that was installed, so changes made upstream since then don't look like deletions.
|
|
201
|
+
const vf = path.join(dir, "VERSION");
|
|
202
|
+
const version = fs.existsSync(vf) ? fs.readFileSync(vf, "utf8").trim() : null;
|
|
203
|
+
const tag = version ? `v${version}` : null;
|
|
204
|
+
const hasTag = tag && tryRun("git", ["rev-parse", "--verify", "--quiet", `refs/tags/${tag}`], work) !== null;
|
|
205
|
+
const branch = `improve/${(opts.title || "changes").toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "").slice(0, 40) || "changes"}-${Date.now().toString(36)}`;
|
|
206
|
+
run("git", hasTag ? ["checkout", "-b", branch, tag] : ["checkout", "-b", branch], work);
|
|
207
|
+
|
|
208
|
+
for (const p of SKILL_PATHS) fs.rmSync(path.join(work, p), { recursive: true, force: true });
|
|
209
|
+
copySkill(dir, work);
|
|
210
|
+
run("git", ["add", "-A", "--", ...SKILL_PATHS], work);
|
|
211
|
+
|
|
212
|
+
const stat = run("git", ["diff", "--cached", "--stat"], work);
|
|
213
|
+
if (!stat) {
|
|
214
|
+
console.log(`No changes: your copy matches ${hasTag ? tag : "the current skill"}.`);
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
const diff = run("git", ["diff", "--cached", "-U2"], work);
|
|
218
|
+
const hits = scanAdded(diff);
|
|
219
|
+
if (hits.length) die(`personal details in your changes. Remove them and run this again. Nothing was sent.\n ${hits.join("\n ")}`, 2);
|
|
220
|
+
|
|
221
|
+
if (!opts.yes) {
|
|
222
|
+
console.log(diff);
|
|
223
|
+
console.log(`\n${stat}\n\nDRY RUN: nothing was sent. Read the whole diff, since names can't be detected automatically.`);
|
|
224
|
+
console.log('To send it, with the owner\'s agreement: npx srm-report contribute --yes --title "What improved" --body "Why"');
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
run("git", ["commit", "-m", opts.title], work);
|
|
228
|
+
run("git", ["push", "-u", "origin", branch], work);
|
|
229
|
+
const head = own ? branch : `${login}:${branch}`;
|
|
230
|
+
const body = opts.body || `Improvements found while making a report.\n\n\`\`\`\n${stat}\n\`\`\``;
|
|
231
|
+
console.log(run("gh", ["pr", "create", "--repo", `${OWNER}/${REPO}`, "--base", "main", "--head", head, "--title", opts.title, "--body", body], work));
|
|
232
|
+
} catch (e) {
|
|
233
|
+
die(((e.stderr && e.stderr.toString()) || e.message).trim());
|
|
234
|
+
} finally {
|
|
235
|
+
fs.rmSync(tmp, { recursive: true, force: true });
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
const opts = parse(process.argv.slice(2));
|
|
240
|
+
const cmd = opts.help ? "help" : opts._[0] || "help";
|
|
241
|
+
if (cmd === "install") install(opts);
|
|
242
|
+
else if (cmd === "contribute") contribute(opts);
|
|
243
|
+
else if (cmd === "help") console.log(HELP);
|
|
244
|
+
else die(`unknown command "${cmd}"\n\n${HELP}`);
|