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 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 -y # Skip all prompts (install all targets, append main docs, overwrite others)
57
- npx updose add owner/repo-name --dry-run # Preview what would be installed without writing any files
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. Fetches the boilerplate's `updose.json` manifest from the GitHub repository
63
- 2. 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.
64
- 3. Downloads the file tree from the repository and filters files for the selected target(s)
65
- 4. Installs each file into your project. If a file already exists, the prompt depends on the file type:
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
- 5. If a `skills.json` file exists in the boilerplate, 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`)
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 | Current directory 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 repository owner and name (case-insensitive). If they don't match, shows an error and exits.
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: `Users can now install with: npx updose add owner/repo`
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
- After publishing, anyone can install your boilerplate with `npx updose add your-username/my-boilerplate`.
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 content = await fetchFile(repo, MANIFEST_FILENAME);
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
- return fetchFile(repo, SKILLS_FILENAME);
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
- throw new Error(`Invalid skill command: "${command}"`);
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
- validateCommand([exe, ...args]);
490
- (0, import_node_child_process.execSync)([exe, ...args].join(" "), {
491
- cwd,
492
- stdio: "inherit"
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
- async function addCommand(repo, options) {
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
- for (const skill of skillsManifest.skills) {
661
- try {
662
- runSkillInstall(skill, cwd, agents);
663
- success(`Installed skill: ${skill}`);
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
- } catch (err) {
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 "${skill}": ${err instanceof Error ? err.message : String(err)}`
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 total = installed + skillsInstalled;
675
- success(`Done! ${total} file(s) installed, ${skipped} skipped.`);
676
- if (total > 0) {
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 defaultName = (0, import_node_path4.basename)(cwd);
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)(cwd, file.path);
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
- console.log(
882
- ` 1. Edit your boilerplate files in ${targets.map((t) => `${t}/`).join(", ")}`
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
- ` 4. Others can install with: npx updose add ${author}/${name}`
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 import_promises2 = require("fs/promises");
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, import_promises2.readFile)(AUTH_FILE, "utf-8");
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, import_promises2.readFile)(AUTH_FILE, "utf-8");
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, import_promises2.mkdir)(AUTH_DIR, { recursive: true });
981
- await (0, import_promises2.writeFile)(
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, import_promises2.unlink)(AUTH_FILE);
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 import_promises3 = require("fs/promises");
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, import_promises3.readFile)((0, import_node_path6.join)(cwd, MANIFEST_FILENAME), "utf-8");
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 current directory. Run \`updose init\` first.`
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
- if (manifest.name.toLowerCase() !== repoName.toLowerCase()) {
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 repository name "${repoName}".`
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
- info(`Users can now install with: ${import_chalk3.default.cyan(`npx updose add ${repo}`)}`);
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
- console.log(` ${import_chalk4.default.dim(bp.repo)}`);
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.1.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 content = await fetchFile(repo, MANIFEST_FILENAME);
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
- return fetchFile(repo, SKILLS_FILENAME);
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 { execSync } from "child_process";
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
- throw new Error(`Invalid skill command: "${command}"`);
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
- validateCommand([exe, ...args]);
468
- execSync([exe, ...args].join(" "), {
469
- cwd,
470
- stdio: "inherit"
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
- async function addCommand(repo, options) {
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
- for (const skill of skillsManifest.skills) {
639
- try {
640
- runSkillInstall(skill, cwd, agents);
641
- success(`Installed skill: ${skill}`);
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
- } catch (err) {
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 "${skill}": ${err instanceof Error ? err.message : String(err)}`
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 total = installed + skillsInstalled;
653
- success(`Done! ${total} file(s) installed, ${skipped} skipped.`);
654
- if (total > 0) {
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 as execSync2 } from "child_process";
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 = execSync2("git config github.user", {
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 = execSync2("gh api user --jq .login", {
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 defaultName = basename(cwd);
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(cwd, file.path);
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
- console.log(
860
- ` 1. Edit your boilerplate files in ${targets.map((t) => `${t}/`).join(", ")}`
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
- ` 4. Others can install with: npx updose add ${author}/${name}`
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 mkdir2, readFile as readFile2, unlink, writeFile as writeFile2 } from "fs/promises";
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 mkdir2(AUTH_DIR, { recursive: true });
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 execSync3 } from "child_process";
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(join4(cwd, MANIFEST_FILENAME), "utf-8");
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 current directory. Run \`updose init\` first.`
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
- if (manifest.name.toLowerCase() !== repoName.toLowerCase()) {
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 repository name "${repoName}".`
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
- info(`Users can now install with: ${chalk3.cyan(`npx updose add ${repo}`)}`);
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 = execSync3("git remote get-url origin", {
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
- console.log(` ${chalk4.dim(bp.repo)}`);
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.1.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();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "updose",
3
- "version": "0.1.0",
3
+ "version": "0.3.0",
4
4
  "type": "module",
5
5
  "description": "AI coding tool boilerplate marketplace",
6
6
  "main": "dist/index.cjs",