updose 0.1.0 → 0.3.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 +177 -21
- package/dist/index.cjs +225 -55
- package/dist/index.js +226 -56
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -31,6 +31,7 @@ npx updose <command>
|
|
|
31
31
|
- [`skills.json` Reference](#skillsjson-reference)
|
|
32
32
|
- [Example: Single-Target Boilerplate (Claude)](#example-single-target-boilerplate-claude)
|
|
33
33
|
- [Example: Multi-Target Boilerplate](#example-multi-target-boilerplate)
|
|
34
|
+
- [Monorepo Support](#monorepo-support)
|
|
34
35
|
- [Publishing](#publishing)
|
|
35
36
|
- [License](#license)
|
|
36
37
|
|
|
@@ -43,6 +44,8 @@ npx updose search react
|
|
|
43
44
|
# Install a boilerplate from a GitHub repository
|
|
44
45
|
npx updose add owner/repo-name
|
|
45
46
|
|
|
47
|
+
# Install from a subdirectory within a monorepo
|
|
48
|
+
npx updose add owner/repo-name/nextjs
|
|
46
49
|
```
|
|
47
50
|
|
|
48
51
|
## Commands
|
|
@@ -51,21 +54,29 @@ npx updose add owner/repo-name
|
|
|
51
54
|
|
|
52
55
|
Install a boilerplate from a GitHub repository into your project.
|
|
53
56
|
|
|
57
|
+
The `<repo>` argument accepts two formats:
|
|
58
|
+
|
|
59
|
+
- **`owner/repo`** — installs from the repository root (standard boilerplate)
|
|
60
|
+
- **`owner/repo/dir`** — installs from a subdirectory within the repository (monorepo boilerplate). The `dir` can be nested (e.g., `owner/repo/templates/v2`).
|
|
61
|
+
|
|
54
62
|
```bash
|
|
55
|
-
npx updose add owner/repo-name
|
|
56
|
-
npx updose add owner/repo-name
|
|
57
|
-
npx updose add owner/repo-name
|
|
63
|
+
npx updose add owner/repo-name # Install from repository root
|
|
64
|
+
npx updose add owner/repo-name/nextjs # Install from "nextjs" subdirectory
|
|
65
|
+
npx updose add owner/repo-name/templates/v2 # Install from nested subdirectory
|
|
66
|
+
npx updose add owner/repo-name -y # Skip all prompts
|
|
67
|
+
npx updose add owner/repo-name/nextjs --dry-run # Preview monorepo install
|
|
58
68
|
```
|
|
59
69
|
|
|
60
70
|
**What happens when you run `add`:**
|
|
61
71
|
|
|
62
|
-
1.
|
|
63
|
-
2.
|
|
64
|
-
3.
|
|
65
|
-
4.
|
|
72
|
+
1. Parses the `<repo>` argument to determine the repository (`owner/repo`) and optional subdirectory
|
|
73
|
+
2. Fetches the boilerplate's `updose.json` manifest from the repository root or the specified subdirectory
|
|
74
|
+
3. If the boilerplate supports multiple targets (e.g., both Claude and Gemini), prompts you to choose which targets to install. With `-y`, all targets are installed automatically.
|
|
75
|
+
4. Downloads the file tree from the repository and filters files for the selected target(s). When a subdirectory is specified, only files under that subdirectory are considered.
|
|
76
|
+
5. Installs each file into your project. If a file already exists, the prompt depends on the file type:
|
|
66
77
|
- **Main docs** (`CLAUDE.md`, `AGENTS.md`, `GEMINI.md`): **Append** / **Overwrite** / **Skip**
|
|
67
78
|
- **Other files** (rules, commands, agents, etc.): **Overwrite** / **Skip**
|
|
68
|
-
|
|
79
|
+
6. If a `skills.json` file exists in the boilerplate (or its subdirectory), installs each declared skill via [skills.sh](https://skills.sh/). Skills are installed for the selected targets (`-a`), copied into the project (`--copy`), and auto-confirmed (`-y`)
|
|
69
80
|
|
|
70
81
|
| Option | Description |
|
|
71
82
|
|-------- |------------- |
|
|
@@ -138,6 +149,17 @@ mkdir my-boilerplate && cd my-boilerplate
|
|
|
138
149
|
npx updose init
|
|
139
150
|
```
|
|
140
151
|
|
|
152
|
+
To scaffold inside a subdirectory (for monorepo setups), use the `--dir` option:
|
|
153
|
+
|
|
154
|
+
```bash
|
|
155
|
+
npx updose init --dir nextjs # Creates boilerplate in the "nextjs" subdirectory
|
|
156
|
+
npx updose init --dir templates/v2 # Nested subdirectory is also supported
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
| Option | Description |
|
|
160
|
+
|-------- |------------- |
|
|
161
|
+
| `--dir <dir>` | Create the boilerplate inside the specified subdirectory instead of the repository root. The directory is created if it doesn't exist. |
|
|
162
|
+
|
|
141
163
|
**What happens when you run `init`:**
|
|
142
164
|
|
|
143
165
|
1. Prompts you for boilerplate configuration (see below)
|
|
@@ -145,11 +167,13 @@ npx updose init
|
|
|
145
167
|
3. If a file already exists, asks whether to **Overwrite** or **Skip**
|
|
146
168
|
4. Shows next steps for publishing
|
|
147
169
|
|
|
170
|
+
When `--dir` is used, all generated files are placed inside the specified subdirectory instead of the repository root.
|
|
171
|
+
|
|
148
172
|
**Interactive prompts:**
|
|
149
173
|
|
|
150
174
|
| Prompt | Description | Default |
|
|
151
175
|
|-------- |------------- |--------- |
|
|
152
|
-
| **Name** | Boilerplate name |
|
|
176
|
+
| **Name** | Boilerplate name | Without `--dir`: current directory name (e.g., `my-boilerplate`). With `--dir`: `<repo>/<dir>` (e.g., `my-boilerplate/nextjs`) |
|
|
153
177
|
| **Description** | Short description (optional) | — |
|
|
154
178
|
| **Author** | GitHub username | Auto-detected from `git config github.user` or `gh api user`. If neither is available, you enter it manually. |
|
|
155
179
|
| **Targets** | Which AI tools to support (multiselect) | All selected (`claude`, `codex`, `gemini`) |
|
|
@@ -172,7 +196,7 @@ Plus target-specific directories based on your selection:
|
|
|
172
196
|
| Codex | `codex/AGENTS.md` |
|
|
173
197
|
| Gemini | `gemini/GEMINI.md`, `gemini/skills/` |
|
|
174
198
|
|
|
175
|
-
**Example — scaffolding with all targets selected:**
|
|
199
|
+
**Example — scaffolding with all targets selected (no `--dir`):**
|
|
176
200
|
|
|
177
201
|
```
|
|
178
202
|
my-boilerplate/
|
|
@@ -191,27 +215,57 @@ my-boilerplate/
|
|
|
191
215
|
└── skills/
|
|
192
216
|
```
|
|
193
217
|
|
|
218
|
+
**Example — scaffolding with `--dir nextjs`:**
|
|
219
|
+
|
|
220
|
+
```
|
|
221
|
+
my-monorepo/
|
|
222
|
+
├── nextjs/ ← created by --dir
|
|
223
|
+
│ ├── updose.json
|
|
224
|
+
│ ├── skills.json
|
|
225
|
+
│ ├── README.md
|
|
226
|
+
│ ├── claude/
|
|
227
|
+
│ │ ├── CLAUDE.md
|
|
228
|
+
│ │ ├── rules/
|
|
229
|
+
│ │ ├── agents/
|
|
230
|
+
│ │ └── skills/
|
|
231
|
+
│ ├── codex/
|
|
232
|
+
│ │ └── AGENTS.md
|
|
233
|
+
│ └── gemini/
|
|
234
|
+
│ ├── GEMINI.md
|
|
235
|
+
│ └── skills/
|
|
236
|
+
├── remix/ ← another boilerplate in the same repo
|
|
237
|
+
│ └── ...
|
|
238
|
+
└── README.md ← repo-level README (not managed by updose)
|
|
239
|
+
```
|
|
240
|
+
|
|
194
241
|
After scaffolding, follow the next steps printed by the command:
|
|
195
242
|
|
|
196
243
|
1. Edit your boilerplate files in each target directory
|
|
197
244
|
2. Push to GitHub
|
|
198
|
-
3. Publish with `npx updose publish`
|
|
199
|
-
4. Others can install with `npx updose add <author>/<name>`
|
|
245
|
+
3. Publish with `npx updose publish` (or `npx updose publish --dir nextjs` for monorepo)
|
|
246
|
+
4. Others can install with `npx updose add <author>/<name>` (or `npx updose add <author>/<repo>/nextjs` for monorepo)
|
|
200
247
|
|
|
201
248
|
### `updose publish`
|
|
202
249
|
|
|
203
250
|
Publish your boilerplate to the marketplace so others can find and install it.
|
|
204
251
|
|
|
205
252
|
```bash
|
|
206
|
-
npx updose publish
|
|
253
|
+
npx updose publish # Publish from repository root
|
|
254
|
+
npx updose publish --dir nextjs # Publish from a subdirectory (monorepo)
|
|
207
255
|
```
|
|
208
256
|
|
|
257
|
+
| Option | Description |
|
|
258
|
+
|-------- |------------- |
|
|
259
|
+
| `--dir <dir>` | Read `updose.json` from the specified subdirectory instead of the repository root. Use this when publishing a monorepo boilerplate. |
|
|
260
|
+
|
|
209
261
|
**What happens when you run `publish`:**
|
|
210
262
|
|
|
211
|
-
1. Reads and parses `updose.json` from the current directory. If the file is missing, shows an error and suggests running `updose init` first.
|
|
263
|
+
1. Reads and parses `updose.json` from the current directory (or the subdirectory specified by `--dir`). If the file is missing, shows an error and suggests running `updose init` first.
|
|
212
264
|
2. Validates the manifest structure (name, author, version, targets are required)
|
|
213
265
|
3. Detects the GitHub repository by running `git remote get-url origin`. Supports both HTTPS (`https://github.com/owner/repo.git`) and SSH (`git@github.com:owner/repo.git`) formats.
|
|
214
|
-
4. Validates that `author` and `name` in `updose.json` match the
|
|
266
|
+
4. Validates that `author` and `name` in `updose.json` match the expected name:
|
|
267
|
+
- Without `--dir`: `name` must match the repository name (e.g., `react-starter`)
|
|
268
|
+
- With `--dir`: `name` must match `<repo>/<dir>` (e.g., `my-monorepo/nextjs`)
|
|
215
269
|
5. Authenticates via GitHub — if no valid token exists, automatically runs the `login` flow (see below)
|
|
216
270
|
6. Verifies the repository actually exists on GitHub. If the repo has not been pushed yet, shows an error and exits.
|
|
217
271
|
7. Displays a publication summary for review:
|
|
@@ -225,13 +279,27 @@ Publishing:
|
|
|
225
279
|
Tags: react, typescript, web
|
|
226
280
|
```
|
|
227
281
|
|
|
282
|
+
When `--dir` is used, the summary also includes a `Directory` field:
|
|
283
|
+
|
|
284
|
+
```
|
|
285
|
+
Publishing:
|
|
286
|
+
Name: my-monorepo/nextjs
|
|
287
|
+
Version: 1.0.0
|
|
288
|
+
Repository: example-user/my-monorepo
|
|
289
|
+
Directory: nextjs
|
|
290
|
+
Targets: claude
|
|
291
|
+
Tags: react, nextjs
|
|
292
|
+
```
|
|
293
|
+
|
|
228
294
|
8. Asks for confirmation: **"Publish to registry?"** (defaults to yes). If declined, displays "Publish cancelled." and exits.
|
|
229
295
|
9. Registers the boilerplate in the marketplace registry
|
|
230
|
-
10. On success, displays
|
|
296
|
+
10. On success, displays the install command:
|
|
297
|
+
- Without `--dir`: `Users can now install with: npx updose add owner/repo`
|
|
298
|
+
- With `--dir`: `Users can now install with: npx updose add owner/repo/dir`
|
|
231
299
|
|
|
232
300
|
**Prerequisites:**
|
|
233
301
|
|
|
234
|
-
- A valid `updose.json` in the current directory (run `updose init` to create one)
|
|
302
|
+
- A valid `updose.json` in the current directory or specified subdirectory (run `updose init` to create one)
|
|
235
303
|
- A GitHub remote (`origin`) configured and pushed to GitHub
|
|
236
304
|
- GitHub authentication (handled automatically if not already logged in)
|
|
237
305
|
|
|
@@ -388,7 +456,7 @@ The manifest file that describes your boilerplate.
|
|
|
388
456
|
|
|
389
457
|
| Field | Required | Description |
|
|
390
458
|
|------- |---------- |------------- |
|
|
391
|
-
| `name` | Yes | The boilerplate name. Must match the GitHub repository name.
|
|
459
|
+
| `name` | Yes | The boilerplate name. Must match the GitHub repository name (e.g., `react-starter`). For monorepo boilerplates, must be `<repo>/<dir>` (e.g., `my-starters/nextjs`). |
|
|
392
460
|
| `author` | Yes | Author name. Must match the GitHub repository owner. |
|
|
393
461
|
| `version` | Yes | Version string following [semver](https://semver.org/) (e.g., `1.0.0`). |
|
|
394
462
|
| `targets` | Yes | Array of supported targets: `"claude"`, `"codex"`, and/or `"gemini"`. |
|
|
@@ -497,16 +565,104 @@ boilerplate-multi-target/
|
|
|
497
565
|
|
|
498
566
|
When a user installs a multi-target boilerplate, they are prompted to choose which targets to install. With `-y`, all targets are installed automatically.
|
|
499
567
|
|
|
568
|
+
### Monorepo Support
|
|
569
|
+
|
|
570
|
+
A single GitHub repository can contain multiple boilerplates, each in its own subdirectory. This is useful when you want to publish several related boilerplates (e.g., framework-specific starters) from one repo.
|
|
571
|
+
|
|
572
|
+
**How it works:**
|
|
573
|
+
|
|
574
|
+
- Each subdirectory is an independent boilerplate with its own `updose.json`, `skills.json`, and target directories
|
|
575
|
+
- The `name` field in `updose.json` must be `<repo>/<dir>` (e.g., `my-starters/nextjs`)
|
|
576
|
+
- Users install with `npx updose add owner/repo/dir` instead of `npx updose add owner/repo`
|
|
577
|
+
|
|
578
|
+
**Monorepo directory structure:**
|
|
579
|
+
|
|
580
|
+
```
|
|
581
|
+
my-starters/ ← GitHub repository root
|
|
582
|
+
├── README.md ← repo-level README (not managed by updose)
|
|
583
|
+
├── nextjs/ ← boilerplate for Next.js
|
|
584
|
+
│ ├── updose.json ← name: "my-starters/nextjs"
|
|
585
|
+
│ ├── skills.json
|
|
586
|
+
│ ├── claude/
|
|
587
|
+
│ │ ├── CLAUDE.md
|
|
588
|
+
│ │ └── rules/
|
|
589
|
+
│ │ └── nextjs-conventions.md
|
|
590
|
+
│ └── gemini/
|
|
591
|
+
│ └── GEMINI.md
|
|
592
|
+
├── remix/ ← boilerplate for Remix
|
|
593
|
+
│ ├── updose.json ← name: "my-starters/remix"
|
|
594
|
+
│ ├── skills.json
|
|
595
|
+
│ └── claude/
|
|
596
|
+
│ ├── CLAUDE.md
|
|
597
|
+
│ └── rules/
|
|
598
|
+
│ └── remix-conventions.md
|
|
599
|
+
└── sveltekit/ ← boilerplate for SvelteKit
|
|
600
|
+
├── updose.json ← name: "my-starters/sveltekit"
|
|
601
|
+
├── skills.json
|
|
602
|
+
└── claude/
|
|
603
|
+
└── CLAUDE.md
|
|
604
|
+
```
|
|
605
|
+
|
|
606
|
+
**`updose.json` for a monorepo boilerplate (`nextjs/updose.json`):**
|
|
607
|
+
|
|
608
|
+
```json
|
|
609
|
+
{
|
|
610
|
+
"name": "my-starters/nextjs",
|
|
611
|
+
"author": "example-user",
|
|
612
|
+
"version": "1.0.0",
|
|
613
|
+
"description": "Next.js boilerplate for Claude and Gemini",
|
|
614
|
+
"targets": ["claude", "gemini"],
|
|
615
|
+
"tags": ["nextjs", "react", "web"]
|
|
616
|
+
}
|
|
617
|
+
```
|
|
618
|
+
|
|
619
|
+
> Note: The `name` field uses the format `<repo>/<dir>` (e.g., `my-starters/nextjs`), not just the directory name.
|
|
620
|
+
|
|
621
|
+
**Workflow for creating a monorepo boilerplate:**
|
|
622
|
+
|
|
623
|
+
```bash
|
|
624
|
+
# 1. Scaffold each boilerplate in its own subdirectory
|
|
625
|
+
npx updose init --dir nextjs
|
|
626
|
+
npx updose init --dir remix
|
|
627
|
+
npx updose init --dir sveltekit
|
|
628
|
+
|
|
629
|
+
# 2. Edit each boilerplate's files
|
|
630
|
+
# (edit nextjs/claude/CLAUDE.md, remix/claude/CLAUDE.md, etc.)
|
|
631
|
+
|
|
632
|
+
# 3. Push to GitHub
|
|
633
|
+
git add . && git commit -m "Add boilerplates" && git push
|
|
634
|
+
|
|
635
|
+
# 4. Publish each boilerplate separately
|
|
636
|
+
npx updose publish --dir nextjs
|
|
637
|
+
npx updose publish --dir remix
|
|
638
|
+
npx updose publish --dir sveltekit
|
|
639
|
+
```
|
|
640
|
+
|
|
641
|
+
**Users install each boilerplate independently:**
|
|
642
|
+
|
|
643
|
+
```bash
|
|
644
|
+
npx updose add example-user/my-starters/nextjs
|
|
645
|
+
npx updose add example-user/my-starters/remix
|
|
646
|
+
```
|
|
647
|
+
|
|
500
648
|
## Publishing
|
|
501
649
|
|
|
502
650
|
To share your boilerplate with others through the marketplace:
|
|
503
651
|
|
|
504
|
-
1. Scaffold with `npx updose init`
|
|
652
|
+
1. Scaffold with `npx updose init` (or `npx updose init --dir <dir>` for monorepo)
|
|
505
653
|
2. Add your content (rules, commands, agents, skills, etc.)
|
|
506
654
|
3. Push to a GitHub repository
|
|
507
|
-
4. Run `npx updose publish`
|
|
655
|
+
4. Run `npx updose publish` (or `npx updose publish --dir <dir>` for monorepo)
|
|
656
|
+
|
|
657
|
+
After publishing, anyone can install your boilerplate:
|
|
508
658
|
|
|
509
|
-
|
|
659
|
+
```bash
|
|
660
|
+
# Standard boilerplate
|
|
661
|
+
npx updose add your-username/my-boilerplate
|
|
662
|
+
|
|
663
|
+
# Monorepo boilerplate
|
|
664
|
+
npx updose add your-username/my-monorepo/nextjs
|
|
665
|
+
```
|
|
510
666
|
|
|
511
667
|
## License
|
|
512
668
|
|
package/dist/index.cjs
CHANGED
|
@@ -51,18 +51,18 @@ async function searchBoilerplates(query, filters) {
|
|
|
51
51
|
}
|
|
52
52
|
return await res.json();
|
|
53
53
|
}
|
|
54
|
-
async function recordDownload(repo) {
|
|
54
|
+
async function recordDownload(repo, dir) {
|
|
55
55
|
await fetch(`${API_BASE_URL}/download`, {
|
|
56
56
|
method: "POST",
|
|
57
57
|
headers: {
|
|
58
58
|
"Content-Type": "application/json",
|
|
59
59
|
"User-Agent": USER_AGENT
|
|
60
60
|
},
|
|
61
|
-
body: JSON.stringify({ repo }),
|
|
61
|
+
body: JSON.stringify({ repo, dir: dir ?? null }),
|
|
62
62
|
signal: AbortSignal.timeout(FETCH_TIMEOUT_MS)
|
|
63
63
|
});
|
|
64
64
|
}
|
|
65
|
-
async function registerBoilerplate(repo, manifest, githubToken) {
|
|
65
|
+
async function registerBoilerplate(repo, manifest, githubToken, dir) {
|
|
66
66
|
const res = await fetch(`${API_BASE_URL}/register`, {
|
|
67
67
|
method: "POST",
|
|
68
68
|
headers: {
|
|
@@ -70,7 +70,7 @@ async function registerBoilerplate(repo, manifest, githubToken) {
|
|
|
70
70
|
Authorization: `Bearer ${githubToken}`,
|
|
71
71
|
"User-Agent": USER_AGENT
|
|
72
72
|
},
|
|
73
|
-
body: JSON.stringify({ repo, manifest }),
|
|
73
|
+
body: JSON.stringify({ repo, manifest, dir: dir ?? null }),
|
|
74
74
|
signal: AbortSignal.timeout(FETCH_TIMEOUT_MS)
|
|
75
75
|
});
|
|
76
76
|
if (!res.ok) {
|
|
@@ -130,6 +130,71 @@ function createSpinner(message) {
|
|
|
130
130
|
}
|
|
131
131
|
};
|
|
132
132
|
}
|
|
133
|
+
function createMultiSpinner(labels) {
|
|
134
|
+
const isTTY = process.stderr.isTTY ?? false;
|
|
135
|
+
const frames = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
|
|
136
|
+
const statuses = labels.map(() => "pending");
|
|
137
|
+
let frameIdx = 0;
|
|
138
|
+
let intervalId = null;
|
|
139
|
+
let started = false;
|
|
140
|
+
function truncate(text) {
|
|
141
|
+
const cols = process.stderr.columns ?? 80;
|
|
142
|
+
return text.length > cols ? `${text.slice(0, cols - 1)}\u2026` : text;
|
|
143
|
+
}
|
|
144
|
+
function render() {
|
|
145
|
+
if (!isTTY) return;
|
|
146
|
+
if (started) {
|
|
147
|
+
process.stderr.write(`\x1B[${labels.length}A`);
|
|
148
|
+
}
|
|
149
|
+
started = true;
|
|
150
|
+
const frame = frames[frameIdx++ % frames.length];
|
|
151
|
+
for (let i = 0; i < labels.length; i++) {
|
|
152
|
+
const status = statuses[i];
|
|
153
|
+
let icon;
|
|
154
|
+
if (status === "success") {
|
|
155
|
+
icon = import_chalk.default.green("\u2713");
|
|
156
|
+
} else if (status === "fail") {
|
|
157
|
+
icon = import_chalk.default.red("\u2717");
|
|
158
|
+
} else {
|
|
159
|
+
icon = import_chalk.default.cyan(frame);
|
|
160
|
+
}
|
|
161
|
+
process.stderr.write(`\x1B[K${icon} ${truncate(labels[i])}
|
|
162
|
+
`);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
return {
|
|
166
|
+
start() {
|
|
167
|
+
if (isTTY) {
|
|
168
|
+
render();
|
|
169
|
+
intervalId = setInterval(render, SPINNER_INTERVAL_MS);
|
|
170
|
+
}
|
|
171
|
+
return this;
|
|
172
|
+
},
|
|
173
|
+
markSuccess(index) {
|
|
174
|
+
statuses[index] = "success";
|
|
175
|
+
if (!isTTY) {
|
|
176
|
+
process.stderr.write(`${import_chalk.default.green("\u2713")} ${labels[index]}
|
|
177
|
+
`);
|
|
178
|
+
}
|
|
179
|
+
},
|
|
180
|
+
markFail(index) {
|
|
181
|
+
statuses[index] = "fail";
|
|
182
|
+
if (!isTTY) {
|
|
183
|
+
process.stderr.write(`${import_chalk.default.red("\u2717")} ${labels[index]}
|
|
184
|
+
`);
|
|
185
|
+
}
|
|
186
|
+
},
|
|
187
|
+
stop() {
|
|
188
|
+
if (intervalId) {
|
|
189
|
+
clearInterval(intervalId);
|
|
190
|
+
intervalId = null;
|
|
191
|
+
}
|
|
192
|
+
if (isTTY) {
|
|
193
|
+
render();
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
};
|
|
197
|
+
}
|
|
133
198
|
|
|
134
199
|
// src/core/targets.ts
|
|
135
200
|
var import_node_path = require("path");
|
|
@@ -243,6 +308,18 @@ function optionalStringArray(obj, key) {
|
|
|
243
308
|
// src/core/github.ts
|
|
244
309
|
var GITHUB_RAW = "https://raw.githubusercontent.com";
|
|
245
310
|
var FETCH_TIMEOUT_MS2 = 3e4;
|
|
311
|
+
function parseRepoInput(input) {
|
|
312
|
+
const parts = input.split("/");
|
|
313
|
+
if (parts.length < 2 || !parts[0] || !parts[1]) {
|
|
314
|
+
throw new Error(
|
|
315
|
+
`Invalid repository format: "${input}". Expected "owner/repo" or "owner/repo/dir".`
|
|
316
|
+
);
|
|
317
|
+
}
|
|
318
|
+
const repo = `${parts[0]}/${parts[1]}`;
|
|
319
|
+
const raw = parts.length > 2 ? parts.slice(2).join("/").replace(/\/+$/, "") : void 0;
|
|
320
|
+
const dir = raw || void 0;
|
|
321
|
+
return { repo, dir };
|
|
322
|
+
}
|
|
246
323
|
function parseRepo(repo) {
|
|
247
324
|
const parts = repo.split("/");
|
|
248
325
|
if (parts.length !== 2 || !parts[0] || !parts[1]) {
|
|
@@ -322,11 +399,12 @@ async function fetchFile(repo, path) {
|
|
|
322
399
|
}
|
|
323
400
|
return res.text();
|
|
324
401
|
}
|
|
325
|
-
async function fetchManifest(repo) {
|
|
326
|
-
const
|
|
402
|
+
async function fetchManifest(repo, dir) {
|
|
403
|
+
const path = dir ? `${dir}/${MANIFEST_FILENAME}` : MANIFEST_FILENAME;
|
|
404
|
+
const content = await fetchFile(repo, path);
|
|
327
405
|
if (content === null) {
|
|
328
406
|
throw new Error(
|
|
329
|
-
`No ${MANIFEST_FILENAME} found in ${repo}. Is this an updose boilerplate?`
|
|
407
|
+
`No ${MANIFEST_FILENAME} found in ${dir ? `${repo}/${dir}` : repo}. Is this an updose boilerplate?`
|
|
330
408
|
);
|
|
331
409
|
}
|
|
332
410
|
let raw;
|
|
@@ -364,8 +442,9 @@ async function fetchRepoTree(repo) {
|
|
|
364
442
|
}
|
|
365
443
|
return data.tree.filter((entry) => entry.type === "blob");
|
|
366
444
|
}
|
|
367
|
-
async function fetchSkillsJson(repo) {
|
|
368
|
-
|
|
445
|
+
async function fetchSkillsJson(repo, dir) {
|
|
446
|
+
const path = dir ? `${dir}/${SKILLS_FILENAME}` : SKILLS_FILENAME;
|
|
447
|
+
return fetchFile(repo, path);
|
|
369
448
|
}
|
|
370
449
|
|
|
371
450
|
// src/core/installer.ts
|
|
@@ -480,18 +559,37 @@ function runSkillInstall(command, cwd, agents) {
|
|
|
480
559
|
const parts = command.split(/\s+/);
|
|
481
560
|
const [exe, ...args] = parts;
|
|
482
561
|
if (!exe) {
|
|
483
|
-
|
|
562
|
+
return Promise.reject(new Error(`Invalid skill command: "${command}"`));
|
|
484
563
|
}
|
|
485
564
|
if (agents.length > 0) {
|
|
486
565
|
args.push("-a", ...agents);
|
|
487
566
|
}
|
|
488
567
|
args.push("--copy", "-y");
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
568
|
+
try {
|
|
569
|
+
validateCommand([exe, ...args]);
|
|
570
|
+
} catch (err) {
|
|
571
|
+
return Promise.reject(err);
|
|
572
|
+
}
|
|
573
|
+
return new Promise((resolve2, reject) => {
|
|
574
|
+
(0, import_node_child_process.exec)([exe, ...args].join(" "), { cwd }, (err) => {
|
|
575
|
+
if (err) {
|
|
576
|
+
reject(err);
|
|
577
|
+
} else {
|
|
578
|
+
resolve2();
|
|
579
|
+
}
|
|
580
|
+
});
|
|
493
581
|
});
|
|
494
582
|
}
|
|
583
|
+
function formatSkillLabel(command) {
|
|
584
|
+
const ghMatch = command.match(
|
|
585
|
+
/github\.com\/([^/\s]+\/[^/\s]+?)(?:\.git)?(?:\s|$)/
|
|
586
|
+
);
|
|
587
|
+
const skillMatch = command.match(/--skill\s+(\S+)/);
|
|
588
|
+
if (ghMatch && skillMatch) {
|
|
589
|
+
return `${ghMatch[1]} > ${skillMatch[1]}`;
|
|
590
|
+
}
|
|
591
|
+
return command.replace(/^npx\s+skills\s+add\s+/, "");
|
|
592
|
+
}
|
|
495
593
|
|
|
496
594
|
// src/utils/path.ts
|
|
497
595
|
var import_node_path3 = require("path");
|
|
@@ -510,15 +608,35 @@ function ensureWithinDir(root, destPath) {
|
|
|
510
608
|
}
|
|
511
609
|
|
|
512
610
|
// src/commands/add.ts
|
|
513
|
-
|
|
611
|
+
var SKILL_CONCURRENCY = 5;
|
|
612
|
+
async function settledPool(tasks, limit) {
|
|
613
|
+
const results = new Array(tasks.length);
|
|
614
|
+
let next = 0;
|
|
615
|
+
async function worker() {
|
|
616
|
+
while (next < tasks.length) {
|
|
617
|
+
const i = next++;
|
|
618
|
+
try {
|
|
619
|
+
results[i] = { status: "fulfilled", value: await tasks[i]() };
|
|
620
|
+
} catch (reason) {
|
|
621
|
+
results[i] = { status: "rejected", reason };
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
await Promise.all(
|
|
626
|
+
Array.from({ length: Math.min(limit, tasks.length) }, worker)
|
|
627
|
+
);
|
|
628
|
+
return results;
|
|
629
|
+
}
|
|
630
|
+
async function addCommand(repoInput, options) {
|
|
514
631
|
try {
|
|
632
|
+
const { repo, dir } = parseRepoInput(repoInput);
|
|
515
633
|
const cwd = process.cwd();
|
|
516
634
|
const skipPrompts = options.yes ?? false;
|
|
517
635
|
const dryRun = options.dryRun ?? false;
|
|
518
636
|
const manifestSpinner = createSpinner("Fetching updose.json...").start();
|
|
519
637
|
let manifest;
|
|
520
638
|
try {
|
|
521
|
-
manifest = await fetchManifest(repo);
|
|
639
|
+
manifest = await fetchManifest(repo, dir);
|
|
522
640
|
manifestSpinner.success(
|
|
523
641
|
`Found ${manifest.name} by ${manifest.author} (v${manifest.version})`
|
|
524
642
|
);
|
|
@@ -551,7 +669,7 @@ async function addCommand(repo, options) {
|
|
|
551
669
|
const filesByTarget = /* @__PURE__ */ new Map();
|
|
552
670
|
for (const target of selectedTargets) {
|
|
553
671
|
const sourceDir = getSourceDir(target);
|
|
554
|
-
const prefix = `${sourceDir}/`;
|
|
672
|
+
const prefix = dir ? `${dir}/${sourceDir}/` : `${sourceDir}/`;
|
|
555
673
|
const files = tree.filter((entry) => {
|
|
556
674
|
if (!entry.path.startsWith(prefix)) return false;
|
|
557
675
|
const relativePath = entry.path.slice(prefix.length);
|
|
@@ -584,7 +702,7 @@ async function addCommand(repo, options) {
|
|
|
584
702
|
dryRunCount++;
|
|
585
703
|
}
|
|
586
704
|
}
|
|
587
|
-
const skillsContent2 = await fetchSkillsJson(repo);
|
|
705
|
+
const skillsContent2 = await fetchSkillsJson(repo, dir);
|
|
588
706
|
if (skillsContent2 !== null) {
|
|
589
707
|
try {
|
|
590
708
|
const skillsManifest = parseSkills(
|
|
@@ -643,7 +761,7 @@ async function addCommand(repo, options) {
|
|
|
643
761
|
}
|
|
644
762
|
}
|
|
645
763
|
let skillsInstalled = 0;
|
|
646
|
-
const skillsContent = await fetchSkillsJson(repo);
|
|
764
|
+
const skillsContent = await fetchSkillsJson(repo, dir);
|
|
647
765
|
if (skillsContent === null) {
|
|
648
766
|
info("No skills.json found \u2014 skipping skills installation.");
|
|
649
767
|
} else {
|
|
@@ -657,24 +775,41 @@ async function addCommand(repo, options) {
|
|
|
657
775
|
console.log();
|
|
658
776
|
info("Installing skills...\n");
|
|
659
777
|
const agents = selectedTargets.map(getAgentName);
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
778
|
+
const labels = skillsManifest.skills.map(formatSkillLabel);
|
|
779
|
+
const spinner = createMultiSpinner(labels).start();
|
|
780
|
+
const results = await settledPool(
|
|
781
|
+
skillsManifest.skills.map(
|
|
782
|
+
(skill, i) => () => runSkillInstall(skill, cwd, agents).then(
|
|
783
|
+
() => spinner.markSuccess(i),
|
|
784
|
+
(err) => {
|
|
785
|
+
spinner.markFail(i);
|
|
786
|
+
throw err;
|
|
787
|
+
}
|
|
788
|
+
)
|
|
789
|
+
),
|
|
790
|
+
SKILL_CONCURRENCY
|
|
791
|
+
);
|
|
792
|
+
spinner.stop();
|
|
793
|
+
for (const result of results) {
|
|
794
|
+
if (result.status === "fulfilled") {
|
|
664
795
|
skillsInstalled++;
|
|
665
|
-
}
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
for (let i = 0; i < results.length; i++) {
|
|
799
|
+
const r = results[i];
|
|
800
|
+
if (r.status === "rejected") {
|
|
666
801
|
warn(
|
|
667
|
-
`Failed to install skill "${
|
|
802
|
+
`Failed to install skill "${skillsManifest.skills[i]}": ${r.reason instanceof Error ? r.reason.message : String(r.reason)}`
|
|
668
803
|
);
|
|
669
804
|
}
|
|
670
805
|
}
|
|
671
806
|
}
|
|
672
807
|
}
|
|
673
808
|
console.log();
|
|
674
|
-
const
|
|
675
|
-
success(`Done! ${
|
|
676
|
-
if (
|
|
677
|
-
await recordDownload(repo).catch(() => {
|
|
809
|
+
const summary = skillsInstalled > 0 ? `${installed} file(s) + ${skillsInstalled} skill(s)` : `${installed} file(s)`;
|
|
810
|
+
success(`Done! ${summary} installed, ${skipped} skipped.`);
|
|
811
|
+
if (installed + skillsInstalled > 0) {
|
|
812
|
+
await recordDownload(repo, dir).catch(() => {
|
|
678
813
|
});
|
|
679
814
|
}
|
|
680
815
|
} catch (err) {
|
|
@@ -687,6 +822,7 @@ async function addCommand(repo, options) {
|
|
|
687
822
|
|
|
688
823
|
// src/commands/init.ts
|
|
689
824
|
var import_node_child_process2 = require("child_process");
|
|
825
|
+
var import_promises2 = require("fs/promises");
|
|
690
826
|
var import_node_path4 = require("path");
|
|
691
827
|
var import_prompts4 = __toESM(require("prompts"), 1);
|
|
692
828
|
var DEFAULT_VERSION = "0.1.0";
|
|
@@ -792,10 +928,16 @@ function buildFileList(name, description, author, targets) {
|
|
|
792
928
|
}
|
|
793
929
|
return files;
|
|
794
930
|
}
|
|
795
|
-
async function initCommand() {
|
|
931
|
+
async function initCommand(options) {
|
|
796
932
|
try {
|
|
797
933
|
const cwd = process.cwd();
|
|
798
|
-
const
|
|
934
|
+
const dir = options.dir;
|
|
935
|
+
const baseDir = dir ? (0, import_node_path4.join)(cwd, dir) : cwd;
|
|
936
|
+
if (dir) {
|
|
937
|
+
await (0, import_promises2.mkdir)(baseDir, { recursive: true });
|
|
938
|
+
}
|
|
939
|
+
const repoName = (0, import_node_path4.basename)(cwd);
|
|
940
|
+
const defaultName = dir ? `${repoName}/${dir}` : repoName;
|
|
799
941
|
const gitUser = getGitHubUsername();
|
|
800
942
|
info("Scaffolding a new updose boilerplate...\n");
|
|
801
943
|
let cancelled = false;
|
|
@@ -852,7 +994,7 @@ async function initCommand() {
|
|
|
852
994
|
let created = 0;
|
|
853
995
|
let skipped = 0;
|
|
854
996
|
for (const file of files) {
|
|
855
|
-
const destPath = (0, import_node_path4.join)(
|
|
997
|
+
const destPath = (0, import_node_path4.join)(baseDir, file.path);
|
|
856
998
|
const exists = await fileExists(destPath);
|
|
857
999
|
if (exists) {
|
|
858
1000
|
const { action } = await (0, import_prompts4.default)({
|
|
@@ -878,14 +1020,21 @@ async function initCommand() {
|
|
|
878
1020
|
success(`Boilerplate scaffolded! (${created} created, ${skipped} skipped)`);
|
|
879
1021
|
console.log();
|
|
880
1022
|
info("Next steps:");
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
);
|
|
1023
|
+
const editDirs = targets.map((t) => dir ? `${dir}/${t}/` : `${t}/`);
|
|
1024
|
+
console.log(` 1. Edit your boilerplate files in ${editDirs.join(", ")}`);
|
|
884
1025
|
console.log(" 2. Push to GitHub");
|
|
885
|
-
console.log(" 3. Publish with: npx updose publish");
|
|
886
1026
|
console.log(
|
|
887
|
-
`
|
|
1027
|
+
` 3. Publish with: npx updose publish${dir ? ` --dir ${dir}` : ""}`
|
|
888
1028
|
);
|
|
1029
|
+
if (dir) {
|
|
1030
|
+
console.log(
|
|
1031
|
+
` 4. Others can install with: npx updose add ${author}/${repoName}/${dir}`
|
|
1032
|
+
);
|
|
1033
|
+
} else {
|
|
1034
|
+
console.log(
|
|
1035
|
+
` 4. Others can install with: npx updose add ${author}/${name}`
|
|
1036
|
+
);
|
|
1037
|
+
}
|
|
889
1038
|
} catch (err) {
|
|
890
1039
|
error(
|
|
891
1040
|
err instanceof Error ? err.message : "An unexpected error occurred during init."
|
|
@@ -895,7 +1044,7 @@ async function initCommand() {
|
|
|
895
1044
|
}
|
|
896
1045
|
|
|
897
1046
|
// src/auth/github-oauth.ts
|
|
898
|
-
var
|
|
1047
|
+
var import_promises3 = require("fs/promises");
|
|
899
1048
|
var import_node_os = require("os");
|
|
900
1049
|
var import_node_path5 = require("path");
|
|
901
1050
|
var import_chalk2 = __toESM(require("chalk"), 1);
|
|
@@ -912,7 +1061,7 @@ var AUTH_FILE_MODE = 384;
|
|
|
912
1061
|
var MAX_POLL_INTERVAL_SEC = 60;
|
|
913
1062
|
async function getStoredToken() {
|
|
914
1063
|
try {
|
|
915
|
-
const content = await (0,
|
|
1064
|
+
const content = await (0, import_promises3.readFile)(AUTH_FILE, "utf-8");
|
|
916
1065
|
const data = JSON.parse(content);
|
|
917
1066
|
return data.github_token ?? null;
|
|
918
1067
|
} catch {
|
|
@@ -921,7 +1070,7 @@ async function getStoredToken() {
|
|
|
921
1070
|
}
|
|
922
1071
|
async function getStoredAuth() {
|
|
923
1072
|
try {
|
|
924
|
-
const content = await (0,
|
|
1073
|
+
const content = await (0, import_promises3.readFile)(AUTH_FILE, "utf-8");
|
|
925
1074
|
return JSON.parse(content);
|
|
926
1075
|
} catch {
|
|
927
1076
|
return null;
|
|
@@ -977,8 +1126,8 @@ async function login() {
|
|
|
977
1126
|
throw new Error("GitHub authorization failed");
|
|
978
1127
|
}
|
|
979
1128
|
const username = await fetchUsername(token);
|
|
980
|
-
await (0,
|
|
981
|
-
await (0,
|
|
1129
|
+
await (0, import_promises3.mkdir)(AUTH_DIR, { recursive: true });
|
|
1130
|
+
await (0, import_promises3.writeFile)(
|
|
982
1131
|
AUTH_FILE,
|
|
983
1132
|
JSON.stringify(
|
|
984
1133
|
{ github_token: token, github_username: username },
|
|
@@ -1063,7 +1212,7 @@ function sleep(ms) {
|
|
|
1063
1212
|
}
|
|
1064
1213
|
async function logout() {
|
|
1065
1214
|
try {
|
|
1066
|
-
await (0,
|
|
1215
|
+
await (0, import_promises3.unlink)(AUTH_FILE);
|
|
1067
1216
|
return true;
|
|
1068
1217
|
} catch {
|
|
1069
1218
|
return false;
|
|
@@ -1098,20 +1247,32 @@ async function logoutCommand() {
|
|
|
1098
1247
|
|
|
1099
1248
|
// src/commands/publish.ts
|
|
1100
1249
|
var import_node_child_process3 = require("child_process");
|
|
1101
|
-
var
|
|
1250
|
+
var import_node_fs = require("fs");
|
|
1251
|
+
var import_promises4 = require("fs/promises");
|
|
1102
1252
|
var import_node_path6 = require("path");
|
|
1103
1253
|
var import_chalk3 = __toESM(require("chalk"), 1);
|
|
1104
1254
|
var FETCH_TIMEOUT_MS3 = 1e4;
|
|
1105
|
-
async function publishCommand() {
|
|
1255
|
+
async function publishCommand(options) {
|
|
1106
1256
|
const cwd = process.cwd();
|
|
1257
|
+
const dir = options.dir;
|
|
1258
|
+
const manifestDir = dir ? (0, import_node_path6.join)(cwd, dir) : cwd;
|
|
1259
|
+
if (dir && !(0, import_node_fs.existsSync)(manifestDir)) {
|
|
1260
|
+
error(`Directory "${dir}" does not exist.`);
|
|
1261
|
+
process.exitCode = 1;
|
|
1262
|
+
return;
|
|
1263
|
+
}
|
|
1107
1264
|
let raw;
|
|
1108
1265
|
try {
|
|
1109
|
-
const content = await (0,
|
|
1266
|
+
const content = await (0, import_promises4.readFile)(
|
|
1267
|
+
(0, import_node_path6.join)(manifestDir, MANIFEST_FILENAME),
|
|
1268
|
+
"utf-8"
|
|
1269
|
+
);
|
|
1110
1270
|
raw = JSON.parse(content);
|
|
1111
1271
|
} catch (err) {
|
|
1112
1272
|
if (err.code === "ENOENT") {
|
|
1273
|
+
const location = dir ? `"${dir}"` : "current directory";
|
|
1113
1274
|
error(
|
|
1114
|
-
`No ${MANIFEST_FILENAME} found in
|
|
1275
|
+
`No ${MANIFEST_FILENAME} found in ${location}. Run \`updose init\` first.`
|
|
1115
1276
|
);
|
|
1116
1277
|
} else {
|
|
1117
1278
|
error(
|
|
@@ -1145,9 +1306,10 @@ async function publishCommand() {
|
|
|
1145
1306
|
process.exitCode = 1;
|
|
1146
1307
|
return;
|
|
1147
1308
|
}
|
|
1148
|
-
|
|
1309
|
+
const expectedName = dir ? `${repoName}/${dir}` : repoName;
|
|
1310
|
+
if (manifest.name.toLowerCase() !== expectedName.toLowerCase()) {
|
|
1149
1311
|
error(
|
|
1150
|
-
`Manifest name "${manifest.name}" does not match
|
|
1312
|
+
`Manifest name "${manifest.name}" does not match expected name "${expectedName}".`
|
|
1151
1313
|
);
|
|
1152
1314
|
process.exitCode = 1;
|
|
1153
1315
|
return;
|
|
@@ -1197,6 +1359,9 @@ Make sure you have pushed your code to GitHub.`
|
|
|
1197
1359
|
console.log(` Name: ${manifest.name}`);
|
|
1198
1360
|
console.log(` Version: ${manifest.version}`);
|
|
1199
1361
|
console.log(` Repository: ${repo}`);
|
|
1362
|
+
if (dir) {
|
|
1363
|
+
console.log(` Directory: ${dir}`);
|
|
1364
|
+
}
|
|
1200
1365
|
console.log(` Targets: ${manifest.targets.join(", ")}`);
|
|
1201
1366
|
if (manifest.tags?.length) {
|
|
1202
1367
|
console.log(` Tags: ${manifest.tags.join(", ")}`);
|
|
@@ -1218,11 +1383,15 @@ Make sure you have pushed your code to GitHub.`
|
|
|
1218
1383
|
targets: manifest.targets,
|
|
1219
1384
|
tags: manifest.tags
|
|
1220
1385
|
},
|
|
1221
|
-
token
|
|
1386
|
+
token,
|
|
1387
|
+
dir
|
|
1222
1388
|
);
|
|
1223
1389
|
spinner.success("Published successfully!");
|
|
1224
1390
|
console.log();
|
|
1225
|
-
|
|
1391
|
+
const installPath = dir ? `${repo}/${dir}` : repo;
|
|
1392
|
+
info(
|
|
1393
|
+
`Users can now install with: ${import_chalk3.default.cyan(`npx updose add ${installPath}`)}`
|
|
1394
|
+
);
|
|
1226
1395
|
} catch (err) {
|
|
1227
1396
|
spinner.fail("Publication failed");
|
|
1228
1397
|
error(err.message);
|
|
@@ -1301,17 +1470,18 @@ function formatResult(bp) {
|
|
|
1301
1470
|
if (bp.tags.length > 0) {
|
|
1302
1471
|
console.log(` ${bp.tags.map((t) => import_chalk4.default.dim(`#${t}`)).join(" ")}`);
|
|
1303
1472
|
}
|
|
1304
|
-
|
|
1473
|
+
const repoPath = bp.dir ? `${bp.repo}/${bp.dir}` : bp.repo;
|
|
1474
|
+
console.log(` ${import_chalk4.default.dim(repoPath)}`);
|
|
1305
1475
|
console.log();
|
|
1306
1476
|
}
|
|
1307
1477
|
|
|
1308
1478
|
// src/index.ts
|
|
1309
1479
|
var program = new import_commander.Command();
|
|
1310
|
-
program.name("updose").description("AI coding tool boilerplate marketplace").version("0.
|
|
1480
|
+
program.name("updose").description("AI coding tool boilerplate marketplace").version("0.3.0");
|
|
1311
1481
|
program.command("add <repo>").description("Install a boilerplate").option("-y, --yes", "Skip all prompts and use defaults").option("--dry-run", "Preview install without writing files").action(addCommand);
|
|
1312
1482
|
program.command("search [query]").description("Search for boilerplates").option("--target <target>", "Filter by target (claude, codex, gemini)").option("--tag <tag>", "Filter by tag").option("--author <author>", "Filter by author").action(searchCommand);
|
|
1313
|
-
program.command("init").description("Scaffold a new boilerplate repository").action(initCommand);
|
|
1314
|
-
program.command("publish").description("Publish your boilerplate to the registry").action(publishCommand);
|
|
1483
|
+
program.command("init").description("Scaffold a new boilerplate repository").option("--dir <dir>", "Create boilerplate in a subdirectory").action(initCommand);
|
|
1484
|
+
program.command("publish").description("Publish your boilerplate to the registry").option("--dir <dir>", "Publish from a subdirectory").action(publishCommand);
|
|
1315
1485
|
program.command("login").description("Log in to GitHub").action(loginCommand);
|
|
1316
1486
|
program.command("logout").description("Log out from GitHub").action(logoutCommand);
|
|
1317
1487
|
program.parse();
|
package/dist/index.js
CHANGED
|
@@ -29,18 +29,18 @@ async function searchBoilerplates(query, filters) {
|
|
|
29
29
|
}
|
|
30
30
|
return await res.json();
|
|
31
31
|
}
|
|
32
|
-
async function recordDownload(repo) {
|
|
32
|
+
async function recordDownload(repo, dir) {
|
|
33
33
|
await fetch(`${API_BASE_URL}/download`, {
|
|
34
34
|
method: "POST",
|
|
35
35
|
headers: {
|
|
36
36
|
"Content-Type": "application/json",
|
|
37
37
|
"User-Agent": USER_AGENT
|
|
38
38
|
},
|
|
39
|
-
body: JSON.stringify({ repo }),
|
|
39
|
+
body: JSON.stringify({ repo, dir: dir ?? null }),
|
|
40
40
|
signal: AbortSignal.timeout(FETCH_TIMEOUT_MS)
|
|
41
41
|
});
|
|
42
42
|
}
|
|
43
|
-
async function registerBoilerplate(repo, manifest, githubToken) {
|
|
43
|
+
async function registerBoilerplate(repo, manifest, githubToken, dir) {
|
|
44
44
|
const res = await fetch(`${API_BASE_URL}/register`, {
|
|
45
45
|
method: "POST",
|
|
46
46
|
headers: {
|
|
@@ -48,7 +48,7 @@ async function registerBoilerplate(repo, manifest, githubToken) {
|
|
|
48
48
|
Authorization: `Bearer ${githubToken}`,
|
|
49
49
|
"User-Agent": USER_AGENT
|
|
50
50
|
},
|
|
51
|
-
body: JSON.stringify({ repo, manifest }),
|
|
51
|
+
body: JSON.stringify({ repo, manifest, dir: dir ?? null }),
|
|
52
52
|
signal: AbortSignal.timeout(FETCH_TIMEOUT_MS)
|
|
53
53
|
});
|
|
54
54
|
if (!res.ok) {
|
|
@@ -108,6 +108,71 @@ function createSpinner(message) {
|
|
|
108
108
|
}
|
|
109
109
|
};
|
|
110
110
|
}
|
|
111
|
+
function createMultiSpinner(labels) {
|
|
112
|
+
const isTTY = process.stderr.isTTY ?? false;
|
|
113
|
+
const frames = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
|
|
114
|
+
const statuses = labels.map(() => "pending");
|
|
115
|
+
let frameIdx = 0;
|
|
116
|
+
let intervalId = null;
|
|
117
|
+
let started = false;
|
|
118
|
+
function truncate(text) {
|
|
119
|
+
const cols = process.stderr.columns ?? 80;
|
|
120
|
+
return text.length > cols ? `${text.slice(0, cols - 1)}\u2026` : text;
|
|
121
|
+
}
|
|
122
|
+
function render() {
|
|
123
|
+
if (!isTTY) return;
|
|
124
|
+
if (started) {
|
|
125
|
+
process.stderr.write(`\x1B[${labels.length}A`);
|
|
126
|
+
}
|
|
127
|
+
started = true;
|
|
128
|
+
const frame = frames[frameIdx++ % frames.length];
|
|
129
|
+
for (let i = 0; i < labels.length; i++) {
|
|
130
|
+
const status = statuses[i];
|
|
131
|
+
let icon;
|
|
132
|
+
if (status === "success") {
|
|
133
|
+
icon = chalk.green("\u2713");
|
|
134
|
+
} else if (status === "fail") {
|
|
135
|
+
icon = chalk.red("\u2717");
|
|
136
|
+
} else {
|
|
137
|
+
icon = chalk.cyan(frame);
|
|
138
|
+
}
|
|
139
|
+
process.stderr.write(`\x1B[K${icon} ${truncate(labels[i])}
|
|
140
|
+
`);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
return {
|
|
144
|
+
start() {
|
|
145
|
+
if (isTTY) {
|
|
146
|
+
render();
|
|
147
|
+
intervalId = setInterval(render, SPINNER_INTERVAL_MS);
|
|
148
|
+
}
|
|
149
|
+
return this;
|
|
150
|
+
},
|
|
151
|
+
markSuccess(index) {
|
|
152
|
+
statuses[index] = "success";
|
|
153
|
+
if (!isTTY) {
|
|
154
|
+
process.stderr.write(`${chalk.green("\u2713")} ${labels[index]}
|
|
155
|
+
`);
|
|
156
|
+
}
|
|
157
|
+
},
|
|
158
|
+
markFail(index) {
|
|
159
|
+
statuses[index] = "fail";
|
|
160
|
+
if (!isTTY) {
|
|
161
|
+
process.stderr.write(`${chalk.red("\u2717")} ${labels[index]}
|
|
162
|
+
`);
|
|
163
|
+
}
|
|
164
|
+
},
|
|
165
|
+
stop() {
|
|
166
|
+
if (intervalId) {
|
|
167
|
+
clearInterval(intervalId);
|
|
168
|
+
intervalId = null;
|
|
169
|
+
}
|
|
170
|
+
if (isTTY) {
|
|
171
|
+
render();
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
};
|
|
175
|
+
}
|
|
111
176
|
|
|
112
177
|
// src/core/targets.ts
|
|
113
178
|
import { join } from "path";
|
|
@@ -221,6 +286,18 @@ function optionalStringArray(obj, key) {
|
|
|
221
286
|
// src/core/github.ts
|
|
222
287
|
var GITHUB_RAW = "https://raw.githubusercontent.com";
|
|
223
288
|
var FETCH_TIMEOUT_MS2 = 3e4;
|
|
289
|
+
function parseRepoInput(input) {
|
|
290
|
+
const parts = input.split("/");
|
|
291
|
+
if (parts.length < 2 || !parts[0] || !parts[1]) {
|
|
292
|
+
throw new Error(
|
|
293
|
+
`Invalid repository format: "${input}". Expected "owner/repo" or "owner/repo/dir".`
|
|
294
|
+
);
|
|
295
|
+
}
|
|
296
|
+
const repo = `${parts[0]}/${parts[1]}`;
|
|
297
|
+
const raw = parts.length > 2 ? parts.slice(2).join("/").replace(/\/+$/, "") : void 0;
|
|
298
|
+
const dir = raw || void 0;
|
|
299
|
+
return { repo, dir };
|
|
300
|
+
}
|
|
224
301
|
function parseRepo(repo) {
|
|
225
302
|
const parts = repo.split("/");
|
|
226
303
|
if (parts.length !== 2 || !parts[0] || !parts[1]) {
|
|
@@ -300,11 +377,12 @@ async function fetchFile(repo, path) {
|
|
|
300
377
|
}
|
|
301
378
|
return res.text();
|
|
302
379
|
}
|
|
303
|
-
async function fetchManifest(repo) {
|
|
304
|
-
const
|
|
380
|
+
async function fetchManifest(repo, dir) {
|
|
381
|
+
const path = dir ? `${dir}/${MANIFEST_FILENAME}` : MANIFEST_FILENAME;
|
|
382
|
+
const content = await fetchFile(repo, path);
|
|
305
383
|
if (content === null) {
|
|
306
384
|
throw new Error(
|
|
307
|
-
`No ${MANIFEST_FILENAME} found in ${repo}. Is this an updose boilerplate?`
|
|
385
|
+
`No ${MANIFEST_FILENAME} found in ${dir ? `${repo}/${dir}` : repo}. Is this an updose boilerplate?`
|
|
308
386
|
);
|
|
309
387
|
}
|
|
310
388
|
let raw;
|
|
@@ -342,8 +420,9 @@ async function fetchRepoTree(repo) {
|
|
|
342
420
|
}
|
|
343
421
|
return data.tree.filter((entry) => entry.type === "blob");
|
|
344
422
|
}
|
|
345
|
-
async function fetchSkillsJson(repo) {
|
|
346
|
-
|
|
423
|
+
async function fetchSkillsJson(repo, dir) {
|
|
424
|
+
const path = dir ? `${dir}/${SKILLS_FILENAME}` : SKILLS_FILENAME;
|
|
425
|
+
return fetchFile(repo, path);
|
|
347
426
|
}
|
|
348
427
|
|
|
349
428
|
// src/core/installer.ts
|
|
@@ -427,7 +506,7 @@ async function resolveConflict(filePath, isMainDoc2, skipPrompts) {
|
|
|
427
506
|
}
|
|
428
507
|
|
|
429
508
|
// src/core/skills.ts
|
|
430
|
-
import {
|
|
509
|
+
import { exec } from "child_process";
|
|
431
510
|
function parseSkills(raw) {
|
|
432
511
|
if (typeof raw !== "object" || raw === null) {
|
|
433
512
|
throw new Error("Invalid skills.json: expected an object");
|
|
@@ -458,18 +537,37 @@ function runSkillInstall(command, cwd, agents) {
|
|
|
458
537
|
const parts = command.split(/\s+/);
|
|
459
538
|
const [exe, ...args] = parts;
|
|
460
539
|
if (!exe) {
|
|
461
|
-
|
|
540
|
+
return Promise.reject(new Error(`Invalid skill command: "${command}"`));
|
|
462
541
|
}
|
|
463
542
|
if (agents.length > 0) {
|
|
464
543
|
args.push("-a", ...agents);
|
|
465
544
|
}
|
|
466
545
|
args.push("--copy", "-y");
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
546
|
+
try {
|
|
547
|
+
validateCommand([exe, ...args]);
|
|
548
|
+
} catch (err) {
|
|
549
|
+
return Promise.reject(err);
|
|
550
|
+
}
|
|
551
|
+
return new Promise((resolve2, reject) => {
|
|
552
|
+
exec([exe, ...args].join(" "), { cwd }, (err) => {
|
|
553
|
+
if (err) {
|
|
554
|
+
reject(err);
|
|
555
|
+
} else {
|
|
556
|
+
resolve2();
|
|
557
|
+
}
|
|
558
|
+
});
|
|
471
559
|
});
|
|
472
560
|
}
|
|
561
|
+
function formatSkillLabel(command) {
|
|
562
|
+
const ghMatch = command.match(
|
|
563
|
+
/github\.com\/([^/\s]+\/[^/\s]+?)(?:\.git)?(?:\s|$)/
|
|
564
|
+
);
|
|
565
|
+
const skillMatch = command.match(/--skill\s+(\S+)/);
|
|
566
|
+
if (ghMatch && skillMatch) {
|
|
567
|
+
return `${ghMatch[1]} > ${skillMatch[1]}`;
|
|
568
|
+
}
|
|
569
|
+
return command.replace(/^npx\s+skills\s+add\s+/, "");
|
|
570
|
+
}
|
|
473
571
|
|
|
474
572
|
// src/utils/path.ts
|
|
475
573
|
import { resolve, sep } from "path";
|
|
@@ -488,15 +586,35 @@ function ensureWithinDir(root, destPath) {
|
|
|
488
586
|
}
|
|
489
587
|
|
|
490
588
|
// src/commands/add.ts
|
|
491
|
-
|
|
589
|
+
var SKILL_CONCURRENCY = 5;
|
|
590
|
+
async function settledPool(tasks, limit) {
|
|
591
|
+
const results = new Array(tasks.length);
|
|
592
|
+
let next = 0;
|
|
593
|
+
async function worker() {
|
|
594
|
+
while (next < tasks.length) {
|
|
595
|
+
const i = next++;
|
|
596
|
+
try {
|
|
597
|
+
results[i] = { status: "fulfilled", value: await tasks[i]() };
|
|
598
|
+
} catch (reason) {
|
|
599
|
+
results[i] = { status: "rejected", reason };
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
await Promise.all(
|
|
604
|
+
Array.from({ length: Math.min(limit, tasks.length) }, worker)
|
|
605
|
+
);
|
|
606
|
+
return results;
|
|
607
|
+
}
|
|
608
|
+
async function addCommand(repoInput, options) {
|
|
492
609
|
try {
|
|
610
|
+
const { repo, dir } = parseRepoInput(repoInput);
|
|
493
611
|
const cwd = process.cwd();
|
|
494
612
|
const skipPrompts = options.yes ?? false;
|
|
495
613
|
const dryRun = options.dryRun ?? false;
|
|
496
614
|
const manifestSpinner = createSpinner("Fetching updose.json...").start();
|
|
497
615
|
let manifest;
|
|
498
616
|
try {
|
|
499
|
-
manifest = await fetchManifest(repo);
|
|
617
|
+
manifest = await fetchManifest(repo, dir);
|
|
500
618
|
manifestSpinner.success(
|
|
501
619
|
`Found ${manifest.name} by ${manifest.author} (v${manifest.version})`
|
|
502
620
|
);
|
|
@@ -529,7 +647,7 @@ async function addCommand(repo, options) {
|
|
|
529
647
|
const filesByTarget = /* @__PURE__ */ new Map();
|
|
530
648
|
for (const target of selectedTargets) {
|
|
531
649
|
const sourceDir = getSourceDir(target);
|
|
532
|
-
const prefix = `${sourceDir}/`;
|
|
650
|
+
const prefix = dir ? `${dir}/${sourceDir}/` : `${sourceDir}/`;
|
|
533
651
|
const files = tree.filter((entry) => {
|
|
534
652
|
if (!entry.path.startsWith(prefix)) return false;
|
|
535
653
|
const relativePath = entry.path.slice(prefix.length);
|
|
@@ -562,7 +680,7 @@ async function addCommand(repo, options) {
|
|
|
562
680
|
dryRunCount++;
|
|
563
681
|
}
|
|
564
682
|
}
|
|
565
|
-
const skillsContent2 = await fetchSkillsJson(repo);
|
|
683
|
+
const skillsContent2 = await fetchSkillsJson(repo, dir);
|
|
566
684
|
if (skillsContent2 !== null) {
|
|
567
685
|
try {
|
|
568
686
|
const skillsManifest = parseSkills(
|
|
@@ -621,7 +739,7 @@ async function addCommand(repo, options) {
|
|
|
621
739
|
}
|
|
622
740
|
}
|
|
623
741
|
let skillsInstalled = 0;
|
|
624
|
-
const skillsContent = await fetchSkillsJson(repo);
|
|
742
|
+
const skillsContent = await fetchSkillsJson(repo, dir);
|
|
625
743
|
if (skillsContent === null) {
|
|
626
744
|
info("No skills.json found \u2014 skipping skills installation.");
|
|
627
745
|
} else {
|
|
@@ -635,24 +753,41 @@ async function addCommand(repo, options) {
|
|
|
635
753
|
console.log();
|
|
636
754
|
info("Installing skills...\n");
|
|
637
755
|
const agents = selectedTargets.map(getAgentName);
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
756
|
+
const labels = skillsManifest.skills.map(formatSkillLabel);
|
|
757
|
+
const spinner = createMultiSpinner(labels).start();
|
|
758
|
+
const results = await settledPool(
|
|
759
|
+
skillsManifest.skills.map(
|
|
760
|
+
(skill, i) => () => runSkillInstall(skill, cwd, agents).then(
|
|
761
|
+
() => spinner.markSuccess(i),
|
|
762
|
+
(err) => {
|
|
763
|
+
spinner.markFail(i);
|
|
764
|
+
throw err;
|
|
765
|
+
}
|
|
766
|
+
)
|
|
767
|
+
),
|
|
768
|
+
SKILL_CONCURRENCY
|
|
769
|
+
);
|
|
770
|
+
spinner.stop();
|
|
771
|
+
for (const result of results) {
|
|
772
|
+
if (result.status === "fulfilled") {
|
|
642
773
|
skillsInstalled++;
|
|
643
|
-
}
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
for (let i = 0; i < results.length; i++) {
|
|
777
|
+
const r = results[i];
|
|
778
|
+
if (r.status === "rejected") {
|
|
644
779
|
warn(
|
|
645
|
-
`Failed to install skill "${
|
|
780
|
+
`Failed to install skill "${skillsManifest.skills[i]}": ${r.reason instanceof Error ? r.reason.message : String(r.reason)}`
|
|
646
781
|
);
|
|
647
782
|
}
|
|
648
783
|
}
|
|
649
784
|
}
|
|
650
785
|
}
|
|
651
786
|
console.log();
|
|
652
|
-
const
|
|
653
|
-
success(`Done! ${
|
|
654
|
-
if (
|
|
655
|
-
await recordDownload(repo).catch(() => {
|
|
787
|
+
const summary = skillsInstalled > 0 ? `${installed} file(s) + ${skillsInstalled} skill(s)` : `${installed} file(s)`;
|
|
788
|
+
success(`Done! ${summary} installed, ${skipped} skipped.`);
|
|
789
|
+
if (installed + skillsInstalled > 0) {
|
|
790
|
+
await recordDownload(repo, dir).catch(() => {
|
|
656
791
|
});
|
|
657
792
|
}
|
|
658
793
|
} catch (err) {
|
|
@@ -664,7 +799,8 @@ async function addCommand(repo, options) {
|
|
|
664
799
|
}
|
|
665
800
|
|
|
666
801
|
// src/commands/init.ts
|
|
667
|
-
import { execSync
|
|
802
|
+
import { execSync } from "child_process";
|
|
803
|
+
import { mkdir as mkdir2 } from "fs/promises";
|
|
668
804
|
import { basename, join as join2 } from "path";
|
|
669
805
|
import prompts2 from "prompts";
|
|
670
806
|
var DEFAULT_VERSION = "0.1.0";
|
|
@@ -698,7 +834,7 @@ Add your Gemini instructions here.
|
|
|
698
834
|
}
|
|
699
835
|
function getGitHubUsername() {
|
|
700
836
|
try {
|
|
701
|
-
const ghUser =
|
|
837
|
+
const ghUser = execSync("git config github.user", {
|
|
702
838
|
encoding: "utf-8",
|
|
703
839
|
stdio: ["pipe", "pipe", "pipe"]
|
|
704
840
|
}).trim();
|
|
@@ -706,7 +842,7 @@ function getGitHubUsername() {
|
|
|
706
842
|
} catch {
|
|
707
843
|
}
|
|
708
844
|
try {
|
|
709
|
-
const login2 =
|
|
845
|
+
const login2 = execSync("gh api user --jq .login", {
|
|
710
846
|
encoding: "utf-8",
|
|
711
847
|
stdio: ["pipe", "pipe", "pipe"],
|
|
712
848
|
timeout: GH_CLI_TIMEOUT_MS
|
|
@@ -770,10 +906,16 @@ function buildFileList(name, description, author, targets) {
|
|
|
770
906
|
}
|
|
771
907
|
return files;
|
|
772
908
|
}
|
|
773
|
-
async function initCommand() {
|
|
909
|
+
async function initCommand(options) {
|
|
774
910
|
try {
|
|
775
911
|
const cwd = process.cwd();
|
|
776
|
-
const
|
|
912
|
+
const dir = options.dir;
|
|
913
|
+
const baseDir = dir ? join2(cwd, dir) : cwd;
|
|
914
|
+
if (dir) {
|
|
915
|
+
await mkdir2(baseDir, { recursive: true });
|
|
916
|
+
}
|
|
917
|
+
const repoName = basename(cwd);
|
|
918
|
+
const defaultName = dir ? `${repoName}/${dir}` : repoName;
|
|
777
919
|
const gitUser = getGitHubUsername();
|
|
778
920
|
info("Scaffolding a new updose boilerplate...\n");
|
|
779
921
|
let cancelled = false;
|
|
@@ -830,7 +972,7 @@ async function initCommand() {
|
|
|
830
972
|
let created = 0;
|
|
831
973
|
let skipped = 0;
|
|
832
974
|
for (const file of files) {
|
|
833
|
-
const destPath = join2(
|
|
975
|
+
const destPath = join2(baseDir, file.path);
|
|
834
976
|
const exists = await fileExists(destPath);
|
|
835
977
|
if (exists) {
|
|
836
978
|
const { action } = await prompts2({
|
|
@@ -856,14 +998,21 @@ async function initCommand() {
|
|
|
856
998
|
success(`Boilerplate scaffolded! (${created} created, ${skipped} skipped)`);
|
|
857
999
|
console.log();
|
|
858
1000
|
info("Next steps:");
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
);
|
|
1001
|
+
const editDirs = targets.map((t) => dir ? `${dir}/${t}/` : `${t}/`);
|
|
1002
|
+
console.log(` 1. Edit your boilerplate files in ${editDirs.join(", ")}`);
|
|
862
1003
|
console.log(" 2. Push to GitHub");
|
|
863
|
-
console.log(" 3. Publish with: npx updose publish");
|
|
864
1004
|
console.log(
|
|
865
|
-
`
|
|
1005
|
+
` 3. Publish with: npx updose publish${dir ? ` --dir ${dir}` : ""}`
|
|
866
1006
|
);
|
|
1007
|
+
if (dir) {
|
|
1008
|
+
console.log(
|
|
1009
|
+
` 4. Others can install with: npx updose add ${author}/${repoName}/${dir}`
|
|
1010
|
+
);
|
|
1011
|
+
} else {
|
|
1012
|
+
console.log(
|
|
1013
|
+
` 4. Others can install with: npx updose add ${author}/${name}`
|
|
1014
|
+
);
|
|
1015
|
+
}
|
|
867
1016
|
} catch (err) {
|
|
868
1017
|
error(
|
|
869
1018
|
err instanceof Error ? err.message : "An unexpected error occurred during init."
|
|
@@ -873,7 +1022,7 @@ async function initCommand() {
|
|
|
873
1022
|
}
|
|
874
1023
|
|
|
875
1024
|
// src/auth/github-oauth.ts
|
|
876
|
-
import { mkdir as
|
|
1025
|
+
import { mkdir as mkdir3, readFile as readFile2, unlink, writeFile as writeFile2 } from "fs/promises";
|
|
877
1026
|
import { homedir } from "os";
|
|
878
1027
|
import { join as join3 } from "path";
|
|
879
1028
|
import chalk2 from "chalk";
|
|
@@ -955,7 +1104,7 @@ async function login() {
|
|
|
955
1104
|
throw new Error("GitHub authorization failed");
|
|
956
1105
|
}
|
|
957
1106
|
const username = await fetchUsername(token);
|
|
958
|
-
await
|
|
1107
|
+
await mkdir3(AUTH_DIR, { recursive: true });
|
|
959
1108
|
await writeFile2(
|
|
960
1109
|
AUTH_FILE,
|
|
961
1110
|
JSON.stringify(
|
|
@@ -1075,21 +1224,33 @@ async function logoutCommand() {
|
|
|
1075
1224
|
}
|
|
1076
1225
|
|
|
1077
1226
|
// src/commands/publish.ts
|
|
1078
|
-
import { execSync as
|
|
1227
|
+
import { execSync as execSync2 } from "child_process";
|
|
1228
|
+
import { existsSync } from "fs";
|
|
1079
1229
|
import { readFile as readFile3 } from "fs/promises";
|
|
1080
1230
|
import { join as join4 } from "path";
|
|
1081
1231
|
import chalk3 from "chalk";
|
|
1082
1232
|
var FETCH_TIMEOUT_MS3 = 1e4;
|
|
1083
|
-
async function publishCommand() {
|
|
1233
|
+
async function publishCommand(options) {
|
|
1084
1234
|
const cwd = process.cwd();
|
|
1235
|
+
const dir = options.dir;
|
|
1236
|
+
const manifestDir = dir ? join4(cwd, dir) : cwd;
|
|
1237
|
+
if (dir && !existsSync(manifestDir)) {
|
|
1238
|
+
error(`Directory "${dir}" does not exist.`);
|
|
1239
|
+
process.exitCode = 1;
|
|
1240
|
+
return;
|
|
1241
|
+
}
|
|
1085
1242
|
let raw;
|
|
1086
1243
|
try {
|
|
1087
|
-
const content = await readFile3(
|
|
1244
|
+
const content = await readFile3(
|
|
1245
|
+
join4(manifestDir, MANIFEST_FILENAME),
|
|
1246
|
+
"utf-8"
|
|
1247
|
+
);
|
|
1088
1248
|
raw = JSON.parse(content);
|
|
1089
1249
|
} catch (err) {
|
|
1090
1250
|
if (err.code === "ENOENT") {
|
|
1251
|
+
const location = dir ? `"${dir}"` : "current directory";
|
|
1091
1252
|
error(
|
|
1092
|
-
`No ${MANIFEST_FILENAME} found in
|
|
1253
|
+
`No ${MANIFEST_FILENAME} found in ${location}. Run \`updose init\` first.`
|
|
1093
1254
|
);
|
|
1094
1255
|
} else {
|
|
1095
1256
|
error(
|
|
@@ -1123,9 +1284,10 @@ async function publishCommand() {
|
|
|
1123
1284
|
process.exitCode = 1;
|
|
1124
1285
|
return;
|
|
1125
1286
|
}
|
|
1126
|
-
|
|
1287
|
+
const expectedName = dir ? `${repoName}/${dir}` : repoName;
|
|
1288
|
+
if (manifest.name.toLowerCase() !== expectedName.toLowerCase()) {
|
|
1127
1289
|
error(
|
|
1128
|
-
`Manifest name "${manifest.name}" does not match
|
|
1290
|
+
`Manifest name "${manifest.name}" does not match expected name "${expectedName}".`
|
|
1129
1291
|
);
|
|
1130
1292
|
process.exitCode = 1;
|
|
1131
1293
|
return;
|
|
@@ -1175,6 +1337,9 @@ Make sure you have pushed your code to GitHub.`
|
|
|
1175
1337
|
console.log(` Name: ${manifest.name}`);
|
|
1176
1338
|
console.log(` Version: ${manifest.version}`);
|
|
1177
1339
|
console.log(` Repository: ${repo}`);
|
|
1340
|
+
if (dir) {
|
|
1341
|
+
console.log(` Directory: ${dir}`);
|
|
1342
|
+
}
|
|
1178
1343
|
console.log(` Targets: ${manifest.targets.join(", ")}`);
|
|
1179
1344
|
if (manifest.tags?.length) {
|
|
1180
1345
|
console.log(` Tags: ${manifest.tags.join(", ")}`);
|
|
@@ -1196,11 +1361,15 @@ Make sure you have pushed your code to GitHub.`
|
|
|
1196
1361
|
targets: manifest.targets,
|
|
1197
1362
|
tags: manifest.tags
|
|
1198
1363
|
},
|
|
1199
|
-
token
|
|
1364
|
+
token,
|
|
1365
|
+
dir
|
|
1200
1366
|
);
|
|
1201
1367
|
spinner.success("Published successfully!");
|
|
1202
1368
|
console.log();
|
|
1203
|
-
|
|
1369
|
+
const installPath = dir ? `${repo}/${dir}` : repo;
|
|
1370
|
+
info(
|
|
1371
|
+
`Users can now install with: ${chalk3.cyan(`npx updose add ${installPath}`)}`
|
|
1372
|
+
);
|
|
1204
1373
|
} catch (err) {
|
|
1205
1374
|
spinner.fail("Publication failed");
|
|
1206
1375
|
error(err.message);
|
|
@@ -1209,7 +1378,7 @@ Make sure you have pushed your code to GitHub.`
|
|
|
1209
1378
|
}
|
|
1210
1379
|
function detectRepo(cwd) {
|
|
1211
1380
|
try {
|
|
1212
|
-
const remoteUrl =
|
|
1381
|
+
const remoteUrl = execSync2("git remote get-url origin", {
|
|
1213
1382
|
cwd,
|
|
1214
1383
|
encoding: "utf-8",
|
|
1215
1384
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -1279,17 +1448,18 @@ function formatResult(bp) {
|
|
|
1279
1448
|
if (bp.tags.length > 0) {
|
|
1280
1449
|
console.log(` ${bp.tags.map((t) => chalk4.dim(`#${t}`)).join(" ")}`);
|
|
1281
1450
|
}
|
|
1282
|
-
|
|
1451
|
+
const repoPath = bp.dir ? `${bp.repo}/${bp.dir}` : bp.repo;
|
|
1452
|
+
console.log(` ${chalk4.dim(repoPath)}`);
|
|
1283
1453
|
console.log();
|
|
1284
1454
|
}
|
|
1285
1455
|
|
|
1286
1456
|
// src/index.ts
|
|
1287
1457
|
var program = new Command();
|
|
1288
|
-
program.name("updose").description("AI coding tool boilerplate marketplace").version("0.
|
|
1458
|
+
program.name("updose").description("AI coding tool boilerplate marketplace").version("0.3.0");
|
|
1289
1459
|
program.command("add <repo>").description("Install a boilerplate").option("-y, --yes", "Skip all prompts and use defaults").option("--dry-run", "Preview install without writing files").action(addCommand);
|
|
1290
1460
|
program.command("search [query]").description("Search for boilerplates").option("--target <target>", "Filter by target (claude, codex, gemini)").option("--tag <tag>", "Filter by tag").option("--author <author>", "Filter by author").action(searchCommand);
|
|
1291
|
-
program.command("init").description("Scaffold a new boilerplate repository").action(initCommand);
|
|
1292
|
-
program.command("publish").description("Publish your boilerplate to the registry").action(publishCommand);
|
|
1461
|
+
program.command("init").description("Scaffold a new boilerplate repository").option("--dir <dir>", "Create boilerplate in a subdirectory").action(initCommand);
|
|
1462
|
+
program.command("publish").description("Publish your boilerplate to the registry").option("--dir <dir>", "Publish from a subdirectory").action(publishCommand);
|
|
1293
1463
|
program.command("login").description("Log in to GitHub").action(loginCommand);
|
|
1294
1464
|
program.command("logout").description("Log out from GitHub").action(logoutCommand);
|
|
1295
1465
|
program.parse();
|