@mhmdreza-rafiei/agentry 0.1.0 → 0.2.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/README.md +205 -41
- package/dist/_chunks/source_parser.mjs +2 -0
- package/dist/_chunks/source_parser2.mjs +74 -0
- package/dist/_chunks/theme.mjs +2 -0
- package/dist/_chunks/theme2.mjs +123 -0
- package/dist/cli.d.mts +1 -0
- package/dist/cli.mjs +1687 -0
- package/package.json +30 -11
- package/bin/agentry.js +0 -257
- package/examples/agents/frontend/frontend-developer/AGENT.md +0 -17
- package/examples/agents/frontend/frontend-developer/agentry.json +0 -5
- package/examples/rules/workflow/ask-dont-guess/RULE.md +0 -15
- package/examples/rules/workflow/ask-dont-guess/agentry.json +0 -5
- package/examples/scripts/env/doctor/agentry.json +0 -5
- package/examples/scripts/env/doctor/run.sh +0 -4
- package/examples/skills/prompt/enhance-prompt/SKILL.md +0 -186
- package/examples/skills/prompt/enhance-prompt/references/TEMPLATE.md +0 -182
- package/profile/backend.json +0 -5
- package/profile/frontend.json +0 -5
- package/src/commands.js +0 -64
- package/src/profile.js +0 -21
- package/src/registry.js +0 -145
- package/src/source.js +0 -52
|
@@ -1,182 +0,0 @@
|
|
|
1
|
-
# enhance-prompt — output template
|
|
2
|
-
|
|
3
|
-
Enhance = clarify + structure. **Never strip user-stated contracts.** Never inject paths/stack. Ask (≤3) when blocked or when a better decision/mode would help — don’t silently override.
|
|
4
|
-
|
|
5
|
-
## Correct output shape
|
|
6
|
-
|
|
7
|
-
```markdown
|
|
8
|
-
# <short title>
|
|
9
|
-
|
|
10
|
-
## Goal
|
|
11
|
-
<what success looks like>
|
|
12
|
-
|
|
13
|
-
## Contract
|
|
14
|
-
<optional — compact endpoint/body/remove-keep lines the user stated>
|
|
15
|
-
|
|
16
|
-
## Constraints
|
|
17
|
-
- <must / must-not — one home; don’t repeat Contract>
|
|
18
|
-
|
|
19
|
-
## Steps
|
|
20
|
-
1. <only if order matters>
|
|
21
|
-
2. …
|
|
22
|
-
|
|
23
|
-
## Related skills
|
|
24
|
-
- `/skill-name` *(load first)* — use for <why tied to goal>
|
|
25
|
-
- `/other` — use for <why>
|
|
26
|
-
|
|
27
|
-
## Output
|
|
28
|
-
<deliverable format>
|
|
29
|
-
|
|
30
|
-
## Open questions
|
|
31
|
-
- <resolve from workspace — don’t guess>
|
|
32
|
-
|
|
33
|
-
## After you finish
|
|
34
|
-
When the work is done, write a summary anyone can understand, while remaining detailed:
|
|
35
|
-
- What changed (added/removed)
|
|
36
|
-
- How to verify
|
|
37
|
-
- Anything still open or risky
|
|
38
|
-
```
|
|
39
|
-
|
|
40
|
-
Omit empty sections. Respect depth dial: `compact` | `standard` | `full`.
|
|
41
|
-
|
|
42
|
-
---
|
|
43
|
-
|
|
44
|
-
## Questions-first shape (pass 0)
|
|
45
|
-
|
|
46
|
-
When critical gaps or better decisions/modes matter:
|
|
47
|
-
|
|
48
|
-
```markdown
|
|
49
|
-
## Understood
|
|
50
|
-
<1–3 lines of intent>
|
|
51
|
-
|
|
52
|
-
## Questions (max 3)
|
|
53
|
-
1. **Mode:** run `compact` (densest), `standard` (default), or `full` (more steps/verify)? — suggestion: `standard` because <why>
|
|
54
|
-
2. **Better decision:** you asked for X; Y is often safer/cleaner because <why>. Use **Y** or keep **X**?
|
|
55
|
-
3. **Missing detail:** <field / success criteria / scope> — options A / B / C?
|
|
56
|
-
|
|
57
|
-
Reply with choices; then I’ll emit the full enhanced prompt.
|
|
58
|
-
```
|
|
59
|
-
|
|
60
|
-
Do not invent stack/paths in questions. Don’t ask if the user said not to, or signal is already enough.
|
|
61
|
-
|
|
62
|
-
---
|
|
63
|
-
|
|
64
|
-
## Example A — casual UI (standard)
|
|
65
|
-
|
|
66
|
-
**In:** `Hey bro, the frontend look mid can you improve it?`
|
|
67
|
-
|
|
68
|
-
**Out (right):**
|
|
69
|
-
|
|
70
|
-
```markdown
|
|
71
|
-
# Improve frontend UI quality
|
|
72
|
-
|
|
73
|
-
## Goal
|
|
74
|
-
Raise frontend visual quality so the first viewport and overall UI feel polished and intentional.
|
|
75
|
-
|
|
76
|
-
## Constraints
|
|
77
|
-
- Preserve existing behavior/content unless change is required for the visual fix
|
|
78
|
-
- Do not invent brand, stack, or design language
|
|
79
|
-
- Prefer target workspace design patterns when present
|
|
80
|
-
- First viewport: brand signal, one headline, short support, CTA group, one dominant visual
|
|
81
|
-
|
|
82
|
-
## Related skills
|
|
83
|
-
- `/frontend-design` *(load first)* — layout and visual polish (if installed)
|
|
84
|
-
|
|
85
|
-
## Output
|
|
86
|
-
UI improvements in the target workspace.
|
|
87
|
-
|
|
88
|
-
## After you finish
|
|
89
|
-
Plain-language summary anyone can follow: what looked weak, what changed, how to check desktop and mobile.
|
|
90
|
-
```
|
|
91
|
-
|
|
92
|
-
**Wrong:** invent React/Tailwind paths under `/Users/...`.
|
|
93
|
-
|
|
94
|
-
---
|
|
95
|
-
|
|
96
|
-
## Example B — ask mode + better decision (before enhance)
|
|
97
|
-
|
|
98
|
-
**In:** `refactor auth otp stuff, make it better`
|
|
99
|
-
|
|
100
|
-
**Questions-first (right):**
|
|
101
|
-
|
|
102
|
-
```markdown
|
|
103
|
-
## Understood
|
|
104
|
-
You want an auth/OTP refactor toward a cleaner flow, but the target shape isn’t specified.
|
|
105
|
-
|
|
106
|
-
## Questions (max 3)
|
|
107
|
-
1. **Mode:** `compact`, `standard`, or `full`? — suggestion: `full` (multi-endpoint refactor).
|
|
108
|
-
2. **Better decision:** collapse to `otp/request` → action endpoints (login | register | password-change) with OTP verified **inside** actions (no public verify)? Or keep a public verify step?
|
|
109
|
-
3. **Register body:** if OTP is inside register, should the body be `{ phone, password, code }` (recommended) or something else you already use?
|
|
110
|
-
|
|
111
|
-
Reply 1–3; then I’ll emit the enhanced prompt with contracts preserved.
|
|
112
|
-
```
|
|
113
|
-
|
|
114
|
-
**Wrong:** silently invent full API + FastAPI stack + `G:\Projects\...` without asking.
|
|
115
|
-
|
|
116
|
-
---
|
|
117
|
-
|
|
118
|
-
## Example C — rich auth: preserve contracts (compact packing)
|
|
119
|
-
|
|
120
|
-
**In (excerpt):** OTP verify internal; `otp/request` → action; register `{ phone, password, code }`; password-change like login + `new_password`; remove public verify + change/reset.
|
|
121
|
-
|
|
122
|
-
**Wrong (diluted):**
|
|
123
|
-
|
|
124
|
-
```markdown
|
|
125
|
-
## Constraints
|
|
126
|
-
- Register: accept phone-number and password only
|
|
127
|
-
```
|
|
128
|
-
|
|
129
|
-
**Right (`compact` / `standard`):**
|
|
130
|
-
|
|
131
|
-
```markdown
|
|
132
|
-
# Auth: OTP internal + collapse password APIs
|
|
133
|
-
|
|
134
|
-
## Goal
|
|
135
|
-
OTP verification is internal only. Public flow: otp/request → action (`login` | `register` | `password-change`). Remove public verify-OTP and separate change/reset password APIs.
|
|
136
|
-
|
|
137
|
-
## Contract
|
|
138
|
-
- `POST /auth/otp/request` — `{ phone, purpose }`
|
|
139
|
-
- `POST /auth/login` — `{ phone, method, secret }`
|
|
140
|
-
- `POST /auth/register` — `{ phone, password, code }` ← OTP inside register
|
|
141
|
-
- `POST /auth/password-change` — `{ phone, method, secret, new_password }`
|
|
142
|
-
- Keep: refresh, logout
|
|
143
|
-
- Remove: public verify-OTP; password change; password reset; otp_proof cookie register flow if present
|
|
144
|
-
|
|
145
|
-
## Constraints
|
|
146
|
-
- Verify-OTP stays internal — do not re-expose
|
|
147
|
-
- Register: no `method`, no `secret`
|
|
148
|
-
- `locale_pref`: prefer `localStorage`, else cookies
|
|
149
|
-
- No invented auth fields/status/error shapes; use target workspace
|
|
150
|
-
- Preserve existing security behavior unless user said otherwise
|
|
151
|
-
- No path/stack paste — load from target workspace
|
|
152
|
-
|
|
153
|
-
## Related skills
|
|
154
|
-
- Rank ≤5 installed skills for auth / API / frontend / tests; mark top as load first
|
|
155
|
-
|
|
156
|
-
## Output
|
|
157
|
-
Backend + frontend matching Contract.
|
|
158
|
-
|
|
159
|
-
## Open questions
|
|
160
|
-
- Authenticated vs unauthenticated password-change: resolve OTP purpose / phone-vs-session from workspace — don’t guess
|
|
161
|
-
|
|
162
|
-
## After you finish
|
|
163
|
-
Plain + detailed summary: before→after flow, endpoints removed vs added (with shapes), how to verify (tests + results), anything still open.
|
|
164
|
-
```
|
|
165
|
-
|
|
166
|
-
---
|
|
167
|
-
|
|
168
|
-
## Example D — path / stack leak anti-pattern
|
|
169
|
-
|
|
170
|
-
**Wrong:** `/home/ubuntu/...`, `/Users/dev/...`, “Stack: Next.js, Prisma…”, README dumps.
|
|
171
|
-
|
|
172
|
-
**Right:** portable Goal/Contract/Constraints; `[use target workspace context]` if a pointer is needed.
|
|
173
|
-
|
|
174
|
-
---
|
|
175
|
-
|
|
176
|
-
## Fidelity gate (before emit)
|
|
177
|
-
|
|
178
|
-
- Fields/endpoints/remove-keep all present?
|
|
179
|
-
- No renames (`phone` not `phone-number`)?
|
|
180
|
-
- Goal ↔ Contract ↔ Steps agree?
|
|
181
|
-
- No silent override (suggestions only via questions)?
|
|
182
|
-
- Deduped; mode respected?
|
package/profile/backend.json
DELETED
package/profile/frontend.json
DELETED
package/src/commands.js
DELETED
|
@@ -1,64 +0,0 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
const fs = require('node:fs');
|
|
4
|
-
const os = require('node:os');
|
|
5
|
-
const path = require('node:path');
|
|
6
|
-
|
|
7
|
-
// Assets install under `<root>/.cursor/<type>/<category>/<name>`.
|
|
8
|
-
// ponytail: single target dir (.cursor) for now; per-agent targets (.claude, .codex)
|
|
9
|
-
// can be added later via a `target` field in each asset's agentry.json.
|
|
10
|
-
function targetRoot({ global: isGlobal, dir } = {}) {
|
|
11
|
-
const base = isGlobal ? os.homedir() : dir || process.cwd();
|
|
12
|
-
return path.join(base, '.cursor');
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
function lockPath(root) {
|
|
16
|
-
return path.join(root, 'agentry.lock.json');
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
function readLock(root) {
|
|
20
|
-
try {
|
|
21
|
-
return JSON.parse(fs.readFileSync(lockPath(root), 'utf8'));
|
|
22
|
-
} catch {
|
|
23
|
-
return { version: 1, items: {} };
|
|
24
|
-
}
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
function writeLock(root, lock) {
|
|
28
|
-
fs.mkdirSync(root, { recursive: true });
|
|
29
|
-
fs.writeFileSync(lockPath(root), JSON.stringify(lock, null, 2) + '\n');
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
function installOne(asset, opts) {
|
|
33
|
-
const root = targetRoot(opts);
|
|
34
|
-
const dest = path.join(root, asset.type, asset.id);
|
|
35
|
-
fs.rmSync(dest, { recursive: true, force: true }); // idempotent re-install
|
|
36
|
-
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
|
37
|
-
fs.cpSync(asset.dir, dest, { recursive: true });
|
|
38
|
-
const lock = readLock(root);
|
|
39
|
-
lock.items[`${asset.type}/${asset.id}`] = { installedAt: new Date().toISOString() };
|
|
40
|
-
writeLock(root, lock);
|
|
41
|
-
return dest;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
// Removes a type, a category, or a single asset from the install target.
|
|
45
|
-
// selector: undefined -> whole type; "category" -> category; "category/name" -> one asset.
|
|
46
|
-
function removeSelection(type, selector, opts) {
|
|
47
|
-
const root = targetRoot(opts);
|
|
48
|
-
const rel = [type, selector].filter(Boolean).join('/');
|
|
49
|
-
const dest = path.join(root, rel);
|
|
50
|
-
const existed = fs.existsSync(dest);
|
|
51
|
-
fs.rmSync(dest, { recursive: true, force: true });
|
|
52
|
-
const lock = readLock(root);
|
|
53
|
-
const removedKeys = [];
|
|
54
|
-
for (const key of Object.keys(lock.items)) {
|
|
55
|
-
if (key === rel || key.startsWith(rel + '/')) {
|
|
56
|
-
delete lock.items[key];
|
|
57
|
-
removedKeys.push(key);
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
writeLock(root, lock);
|
|
61
|
-
return { existed, removedKeys };
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
module.exports = { targetRoot, lockPath, readLock, installOne, removeSelection };
|
package/src/profile.js
DELETED
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
const fs = require('node:fs');
|
|
4
|
-
const path = require('node:path');
|
|
5
|
-
|
|
6
|
-
// Profiles are plain JSON under `<baseDir>/profile/<name>.json`, mapping each
|
|
7
|
-
// asset type to a list of selectors, e.g.:
|
|
8
|
-
// { "repo": "author/repo", "skills": ["prompt/enhance-prompt"], "rules": ["workflow"] }
|
|
9
|
-
// `repo` is optional; the command-line source (when given) takes precedence.
|
|
10
|
-
function loadProfile(name, baseDir) {
|
|
11
|
-
const file = path.join(baseDir || process.cwd(), 'profile', `${name}.json`);
|
|
12
|
-
let cfg;
|
|
13
|
-
try {
|
|
14
|
-
cfg = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
15
|
-
} catch {
|
|
16
|
-
throw new Error(`Profile not found or invalid JSON: ${file}`);
|
|
17
|
-
}
|
|
18
|
-
return { file, cfg };
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
module.exports = { loadProfile };
|
package/src/registry.js
DELETED
|
@@ -1,145 +0,0 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
const fs = require('node:fs');
|
|
4
|
-
const path = require('node:path');
|
|
5
|
-
|
|
6
|
-
// Discovers assets inside a *source root* (a fetched repo or local path).
|
|
7
|
-
// Two discovery modes, unioned:
|
|
8
|
-
// 1) Under <type>/ : <type>/<name>/ or <type>/<category>/<name>/
|
|
9
|
-
// 2) At the repo root (like `npx skills add`), classified by a marker file:
|
|
10
|
-
// <name>/SKILL.md -> skills/<name>
|
|
11
|
-
// <group>/<name>/SKILL.md -> skills/<group>/<name>
|
|
12
|
-
// (AGENT.md -> agents, RULE.md -> rules). Scripts have no root marker.
|
|
13
|
-
const TYPES = ['agents', 'skills', 'rules', 'scripts'];
|
|
14
|
-
const MARKERS = { skills: 'SKILL.md', agents: 'AGENT.md', rules: 'RULE.md', scripts: null };
|
|
15
|
-
|
|
16
|
-
function readJson(file) {
|
|
17
|
-
try {
|
|
18
|
-
return JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
19
|
-
} catch {
|
|
20
|
-
return null;
|
|
21
|
-
}
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
function entries(dir) {
|
|
25
|
-
try {
|
|
26
|
-
return fs.readdirSync(dir, { withFileTypes: true });
|
|
27
|
-
} catch {
|
|
28
|
-
return [];
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
function hasFile(dir) {
|
|
33
|
-
return entries(dir).some((e) => e.isFile());
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
function firstHeading(md) {
|
|
37
|
-
const m = md.match(/^#\s+(.+)$/m);
|
|
38
|
-
return m ? m[1].trim() : '';
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
function frontmatterDescription(md) {
|
|
42
|
-
const fm = md.match(/^---\r?\n([\s\S]*?)\r?\n---/);
|
|
43
|
-
if (!fm) return '';
|
|
44
|
-
const lines = fm[1].split(/\r?\n/);
|
|
45
|
-
for (let i = 0; i < lines.length; i++) {
|
|
46
|
-
const dm = lines[i].match(/^description:\s*(.*)$/);
|
|
47
|
-
if (!dm) continue;
|
|
48
|
-
let val = dm[1].trim();
|
|
49
|
-
if (val === '' || val === '>' || val === '|' || val === '>-' || val === '|-') {
|
|
50
|
-
const collected = [];
|
|
51
|
-
for (let j = i + 1; j < lines.length; j++) {
|
|
52
|
-
if (/^\s+\S/.test(lines[j])) collected.push(lines[j].trim());
|
|
53
|
-
else break;
|
|
54
|
-
}
|
|
55
|
-
val = collected.join(' ');
|
|
56
|
-
}
|
|
57
|
-
return val.trim();
|
|
58
|
-
}
|
|
59
|
-
return '';
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
function assetDescription(dir) {
|
|
63
|
-
const cfg = readJson(path.join(dir, 'agentry.json'));
|
|
64
|
-
if (cfg && cfg.description) return cfg.description;
|
|
65
|
-
for (const f of ['SKILL.md', 'AGENT.md', 'RULE.md', 'README.md']) {
|
|
66
|
-
const p = path.join(dir, f);
|
|
67
|
-
if (!fs.existsSync(p)) continue;
|
|
68
|
-
const md = fs.readFileSync(p, 'utf8');
|
|
69
|
-
const d = frontmatterDescription(md) || firstHeading(md);
|
|
70
|
-
if (d) return d;
|
|
71
|
-
}
|
|
72
|
-
return '';
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
function hasMarker(dir, marker) {
|
|
76
|
-
return !!marker && fs.existsSync(path.join(dir, marker));
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
function makeAsset(type, category, name, dir) {
|
|
80
|
-
const id = category ? `${category}/${name}` : name;
|
|
81
|
-
return { type, category, name, id, dir, description: assetDescription(dir) };
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
// Assets living under an explicit <type>/ folder (this repo's own convention).
|
|
85
|
-
function collectFromTypeDir(base, type, out) {
|
|
86
|
-
for (const child of entries(base)) {
|
|
87
|
-
if (!child.isDirectory()) continue;
|
|
88
|
-
const childDir = path.join(base, child.name);
|
|
89
|
-
if (hasFile(childDir)) {
|
|
90
|
-
out.push(makeAsset(type, null, child.name, childDir));
|
|
91
|
-
} else {
|
|
92
|
-
for (const item of entries(childDir)) {
|
|
93
|
-
if (!item.isDirectory()) continue;
|
|
94
|
-
out.push(makeAsset(type, child.name, item.name, path.join(childDir, item.name)));
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
// Assets at the repo root, identified by marker file (e.g. SKILL.md).
|
|
101
|
-
function collectFromRoot(root, type, marker, out) {
|
|
102
|
-
if (!marker) return;
|
|
103
|
-
for (const child of entries(root)) {
|
|
104
|
-
if (!child.isDirectory()) continue;
|
|
105
|
-
if (child.name.startsWith('.') || child.name === 'node_modules' || TYPES.includes(child.name)) continue;
|
|
106
|
-
const childDir = path.join(root, child.name);
|
|
107
|
-
if (hasMarker(childDir, marker)) {
|
|
108
|
-
out.push(makeAsset(type, null, child.name, childDir));
|
|
109
|
-
} else {
|
|
110
|
-
for (const item of entries(childDir)) {
|
|
111
|
-
if (item.isDirectory() && hasMarker(path.join(childDir, item.name), marker)) {
|
|
112
|
-
out.push(makeAsset(type, child.name, item.name, path.join(childDir, item.name)));
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
function listType(root, type) {
|
|
120
|
-
const out = [];
|
|
121
|
-
const base = path.join(root, type);
|
|
122
|
-
if (fs.existsSync(base)) collectFromTypeDir(base, type, out);
|
|
123
|
-
collectFromRoot(root, type, MARKERS[type], out);
|
|
124
|
-
const seen = new Set();
|
|
125
|
-
return out
|
|
126
|
-
.filter((a) => (seen.has(a.id) ? false : seen.add(a.id)))
|
|
127
|
-
.sort((a, b) => a.id.localeCompare(b.id));
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
function allAssets(root) {
|
|
131
|
-
return TYPES.flatMap((t) => listType(root, t));
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
// selector: undefined -> whole type; "category/name" -> one asset;
|
|
135
|
-
// "category" -> whole category; bare name -> fallback exact-name match.
|
|
136
|
-
function select(root, type, selector) {
|
|
137
|
-
const items = listType(root, type);
|
|
138
|
-
if (!selector) return items;
|
|
139
|
-
if (selector.includes('/')) return items.filter((a) => a.id === selector);
|
|
140
|
-
const byCategory = items.filter((a) => a.category === selector);
|
|
141
|
-
if (byCategory.length) return byCategory;
|
|
142
|
-
return items.filter((a) => a.name === selector);
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
module.exports = { TYPES, listType, allAssets, select };
|
package/src/source.js
DELETED
|
@@ -1,52 +0,0 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
const fs = require('node:fs');
|
|
4
|
-
const os = require('node:os');
|
|
5
|
-
const path = require('node:path');
|
|
6
|
-
const { execFileSync } = require('node:child_process');
|
|
7
|
-
|
|
8
|
-
function isLocalPath(source) {
|
|
9
|
-
return (
|
|
10
|
-
source.startsWith('.') ||
|
|
11
|
-
source.startsWith('/') ||
|
|
12
|
-
source.startsWith('~') ||
|
|
13
|
-
fs.existsSync(source)
|
|
14
|
-
);
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
// Accepts a full git URL or the "author/repo" GitHub shorthand.
|
|
18
|
-
function toGitUrl(source) {
|
|
19
|
-
if (/^https?:\/\//.test(source) || source.startsWith('git@')) return source;
|
|
20
|
-
if (/^[\w.-]+\/[\w.-]+$/.test(source)) return `https://github.com/${source}.git`;
|
|
21
|
-
return null;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
// Resolves a source (local path | author/repo | git URL) to a local root.
|
|
25
|
-
// Remote sources are shallow-cloned to a temp dir; call cleanup() when done.
|
|
26
|
-
function resolveSource(source) {
|
|
27
|
-
if (isLocalPath(source)) {
|
|
28
|
-
const root = path.resolve(source.replace(/^~/, os.homedir()));
|
|
29
|
-
if (!fs.existsSync(root)) throw new Error(`Path not found: ${source}`);
|
|
30
|
-
return { root, cleanup() {} };
|
|
31
|
-
}
|
|
32
|
-
const url = toGitUrl(source);
|
|
33
|
-
if (!url) throw new Error(`Cannot resolve source "${source}". Use a local path, author/repo, or git URL.`);
|
|
34
|
-
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'agentry-src-'));
|
|
35
|
-
try {
|
|
36
|
-
execFileSync('git', ['clone', '--depth', '1', '--quiet', url, tmp], {
|
|
37
|
-
stdio: ['ignore', 'ignore', 'pipe'],
|
|
38
|
-
});
|
|
39
|
-
} catch (e) {
|
|
40
|
-
fs.rmSync(tmp, { recursive: true, force: true });
|
|
41
|
-
const detail = e.stderr ? e.stderr.toString().trim() : e.message;
|
|
42
|
-
throw new Error(`git clone failed for ${url}\n${detail}`);
|
|
43
|
-
}
|
|
44
|
-
return {
|
|
45
|
-
root: tmp,
|
|
46
|
-
cleanup() {
|
|
47
|
-
fs.rmSync(tmp, { recursive: true, force: true });
|
|
48
|
-
},
|
|
49
|
-
};
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
module.exports = { isLocalPath, toGitUrl, resolveSource };
|