updose 0.2.0 → 0.4.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
  |-------- |------------- |
@@ -119,9 +130,9 @@ npx updose search --author james --target claude # james's Claude boilerpla
119
130
  npx updose search --tag typescript --target codex # TypeScript boilerplates for Codex
120
131
  ```
121
132
 
122
- At least one of the query or filter options must be provided. Running `npx updose search` with no arguments will show an error.
133
+ Running `npx updose search` with no arguments returns popular boilerplates.
123
134
 
124
- Results display the boilerplate name, version, author, description, rating, download count, supported targets, and tags.
135
+ Results display the boilerplate name, version, author, description, download count, supported targets, and tags.
125
136
 
126
137
  | Option | Description |
127
138
  |-------- |------------- |
@@ -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
@@ -25,6 +25,9 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
25
25
  // src/index.ts
26
26
  var import_commander = require("commander");
27
27
 
28
+ // src/commands/add.ts
29
+ var import_node_crypto = require("crypto");
30
+
28
31
  // src/constants.ts
29
32
  var USER_AGENT = "updose-cli";
30
33
  var MANIFEST_FILENAME = "updose.json";
@@ -51,18 +54,22 @@ async function searchBoilerplates(query, filters) {
51
54
  }
52
55
  return await res.json();
53
56
  }
54
- async function recordDownload(repo) {
57
+ async function recordDownload(repo, dir, projectHash) {
55
58
  await fetch(`${API_BASE_URL}/download`, {
56
59
  method: "POST",
57
60
  headers: {
58
61
  "Content-Type": "application/json",
59
62
  "User-Agent": USER_AGENT
60
63
  },
61
- body: JSON.stringify({ repo }),
64
+ body: JSON.stringify({
65
+ repo,
66
+ dir: dir ?? null,
67
+ ...projectHash ? { project_hash: projectHash } : {}
68
+ }),
62
69
  signal: AbortSignal.timeout(FETCH_TIMEOUT_MS)
63
70
  });
64
71
  }
65
- async function registerBoilerplate(repo, manifest, githubToken) {
72
+ async function registerBoilerplate(repo, manifest, githubToken, dir) {
66
73
  const res = await fetch(`${API_BASE_URL}/register`, {
67
74
  method: "POST",
68
75
  headers: {
@@ -70,7 +77,7 @@ async function registerBoilerplate(repo, manifest, githubToken) {
70
77
  Authorization: `Bearer ${githubToken}`,
71
78
  "User-Agent": USER_AGENT
72
79
  },
73
- body: JSON.stringify({ repo, manifest }),
80
+ body: JSON.stringify({ repo, manifest, dir: dir ?? null }),
74
81
  signal: AbortSignal.timeout(FETCH_TIMEOUT_MS)
75
82
  });
76
83
  if (!res.ok) {
@@ -308,6 +315,18 @@ function optionalStringArray(obj, key) {
308
315
  // src/core/github.ts
309
316
  var GITHUB_RAW = "https://raw.githubusercontent.com";
310
317
  var FETCH_TIMEOUT_MS2 = 3e4;
318
+ function parseRepoInput(input) {
319
+ const parts = input.split("/");
320
+ if (parts.length < 2 || !parts[0] || !parts[1]) {
321
+ throw new Error(
322
+ `Invalid repository format: "${input}". Expected "owner/repo" or "owner/repo/dir".`
323
+ );
324
+ }
325
+ const repo = `${parts[0]}/${parts[1]}`;
326
+ const raw = parts.length > 2 ? parts.slice(2).join("/").replace(/\/+$/, "") : void 0;
327
+ const dir = raw || void 0;
328
+ return { repo, dir };
329
+ }
311
330
  function parseRepo(repo) {
312
331
  const parts = repo.split("/");
313
332
  if (parts.length !== 2 || !parts[0] || !parts[1]) {
@@ -345,35 +364,10 @@ function handleHttpError(res) {
345
364
  );
346
365
  }
347
366
  }
348
- var branchCache = /* @__PURE__ */ new Map();
349
- async function getDefaultBranch(repo) {
350
- const cached = branchCache.get(repo);
351
- if (cached) return cached;
352
- const { owner, name } = parseRepo(repo);
353
- const res = await fetch(`${GITHUB_API_URL}/repos/${owner}/${name}`, {
354
- headers: {
355
- Accept: GITHUB_ACCEPT_HEADER,
356
- "User-Agent": USER_AGENT,
357
- ...getAuthHeaders()
358
- },
359
- signal: createSignal()
360
- });
361
- if (res.status === 404) {
362
- throw new Error(`Repository not found: ${repo}`);
363
- }
364
- handleHttpError(res);
365
- if (!res.ok) {
366
- throw new Error(`GitHub API error: ${res.status} ${res.statusText}`);
367
- }
368
- const data = await res.json();
369
- branchCache.set(repo, data.default_branch);
370
- return data.default_branch;
371
- }
372
367
  async function fetchFile(repo, path) {
373
368
  const { owner, name } = parseRepo(repo);
374
- const branch = await getDefaultBranch(repo);
375
369
  const encodedPath = path.split("/").map((segment) => encodeURIComponent(segment)).join("/");
376
- const url = `${GITHUB_RAW}/${owner}/${name}/${branch}/${encodedPath}`;
370
+ const url = `${GITHUB_RAW}/${owner}/${name}/HEAD/${encodedPath}`;
377
371
  const res = await fetch(url, {
378
372
  headers: { "User-Agent": USER_AGENT, ...getAuthHeaders() },
379
373
  signal: createSignal()
@@ -387,11 +381,12 @@ async function fetchFile(repo, path) {
387
381
  }
388
382
  return res.text();
389
383
  }
390
- async function fetchManifest(repo) {
391
- const content = await fetchFile(repo, MANIFEST_FILENAME);
384
+ async function fetchManifest(repo, dir) {
385
+ const path = dir ? `${dir}/${MANIFEST_FILENAME}` : MANIFEST_FILENAME;
386
+ const content = await fetchFile(repo, path);
392
387
  if (content === null) {
393
388
  throw new Error(
394
- `No ${MANIFEST_FILENAME} found in ${repo}. Is this an updose boilerplate?`
389
+ `No ${MANIFEST_FILENAME} found in ${dir ? `${repo}/${dir}` : repo}. Is this an updose boilerplate?`
395
390
  );
396
391
  }
397
392
  let raw;
@@ -404,8 +399,7 @@ async function fetchManifest(repo) {
404
399
  }
405
400
  async function fetchRepoTree(repo) {
406
401
  const { owner, name } = parseRepo(repo);
407
- const branch = await getDefaultBranch(repo);
408
- const url = `${GITHUB_API_URL}/repos/${owner}/${name}/git/trees/${branch}?recursive=1`;
402
+ const url = `${GITHUB_API_URL}/repos/${owner}/${name}/git/trees/HEAD?recursive=1`;
409
403
  const res = await fetch(url, {
410
404
  headers: {
411
405
  Accept: GITHUB_ACCEPT_HEADER,
@@ -429,8 +423,9 @@ async function fetchRepoTree(repo) {
429
423
  }
430
424
  return data.tree.filter((entry) => entry.type === "blob");
431
425
  }
432
- async function fetchSkillsJson(repo) {
433
- return fetchFile(repo, SKILLS_FILENAME);
426
+ async function fetchSkillsJson(repo, dir) {
427
+ const path = dir ? `${dir}/${SKILLS_FILENAME}` : SKILLS_FILENAME;
428
+ return fetchFile(repo, path);
434
429
  }
435
430
 
436
431
  // src/core/installer.ts
@@ -613,15 +608,16 @@ async function settledPool(tasks, limit) {
613
608
  );
614
609
  return results;
615
610
  }
616
- async function addCommand(repo, options) {
611
+ async function addCommand(repoInput, options) {
617
612
  try {
613
+ const { repo, dir } = parseRepoInput(repoInput);
618
614
  const cwd = process.cwd();
619
615
  const skipPrompts = options.yes ?? false;
620
616
  const dryRun = options.dryRun ?? false;
621
617
  const manifestSpinner = createSpinner("Fetching updose.json...").start();
622
618
  let manifest;
623
619
  try {
624
- manifest = await fetchManifest(repo);
620
+ manifest = await fetchManifest(repo, dir);
625
621
  manifestSpinner.success(
626
622
  `Found ${manifest.name} by ${manifest.author} (v${manifest.version})`
627
623
  );
@@ -654,7 +650,7 @@ async function addCommand(repo, options) {
654
650
  const filesByTarget = /* @__PURE__ */ new Map();
655
651
  for (const target of selectedTargets) {
656
652
  const sourceDir = getSourceDir(target);
657
- const prefix = `${sourceDir}/`;
653
+ const prefix = dir ? `${dir}/${sourceDir}/` : `${sourceDir}/`;
658
654
  const files = tree.filter((entry) => {
659
655
  if (!entry.path.startsWith(prefix)) return false;
660
656
  const relativePath = entry.path.slice(prefix.length);
@@ -687,7 +683,7 @@ async function addCommand(repo, options) {
687
683
  dryRunCount++;
688
684
  }
689
685
  }
690
- const skillsContent2 = await fetchSkillsJson(repo);
686
+ const skillsContent2 = await fetchSkillsJson(repo, dir);
691
687
  if (skillsContent2 !== null) {
692
688
  try {
693
689
  const skillsManifest = parseSkills(
@@ -746,7 +742,7 @@ async function addCommand(repo, options) {
746
742
  }
747
743
  }
748
744
  let skillsInstalled = 0;
749
- const skillsContent = await fetchSkillsJson(repo);
745
+ const skillsContent = await fetchSkillsJson(repo, dir);
750
746
  if (skillsContent === null) {
751
747
  info("No skills.json found \u2014 skipping skills installation.");
752
748
  } else {
@@ -794,7 +790,8 @@ async function addCommand(repo, options) {
794
790
  const summary = skillsInstalled > 0 ? `${installed} file(s) + ${skillsInstalled} skill(s)` : `${installed} file(s)`;
795
791
  success(`Done! ${summary} installed, ${skipped} skipped.`);
796
792
  if (installed + skillsInstalled > 0) {
797
- await recordDownload(repo).catch(() => {
793
+ const projectHash = (0, import_node_crypto.createHash)("sha256").update(cwd).digest("hex");
794
+ await recordDownload(repo, dir, projectHash).catch(() => {
798
795
  });
799
796
  }
800
797
  } catch (err) {
@@ -807,6 +804,7 @@ async function addCommand(repo, options) {
807
804
 
808
805
  // src/commands/init.ts
809
806
  var import_node_child_process2 = require("child_process");
807
+ var import_promises2 = require("fs/promises");
810
808
  var import_node_path4 = require("path");
811
809
  var import_prompts4 = __toESM(require("prompts"), 1);
812
810
  var DEFAULT_VERSION = "0.1.0";
@@ -912,10 +910,16 @@ function buildFileList(name, description, author, targets) {
912
910
  }
913
911
  return files;
914
912
  }
915
- async function initCommand() {
913
+ async function initCommand(options) {
916
914
  try {
917
915
  const cwd = process.cwd();
918
- const defaultName = (0, import_node_path4.basename)(cwd);
916
+ const dir = options.dir;
917
+ const baseDir = dir ? (0, import_node_path4.join)(cwd, dir) : cwd;
918
+ if (dir) {
919
+ await (0, import_promises2.mkdir)(baseDir, { recursive: true });
920
+ }
921
+ const repoName = (0, import_node_path4.basename)(cwd);
922
+ const defaultName = dir ? `${repoName}/${dir}` : repoName;
919
923
  const gitUser = getGitHubUsername();
920
924
  info("Scaffolding a new updose boilerplate...\n");
921
925
  let cancelled = false;
@@ -972,7 +976,7 @@ async function initCommand() {
972
976
  let created = 0;
973
977
  let skipped = 0;
974
978
  for (const file of files) {
975
- const destPath = (0, import_node_path4.join)(cwd, file.path);
979
+ const destPath = (0, import_node_path4.join)(baseDir, file.path);
976
980
  const exists = await fileExists(destPath);
977
981
  if (exists) {
978
982
  const { action } = await (0, import_prompts4.default)({
@@ -998,14 +1002,21 @@ async function initCommand() {
998
1002
  success(`Boilerplate scaffolded! (${created} created, ${skipped} skipped)`);
999
1003
  console.log();
1000
1004
  info("Next steps:");
1001
- console.log(
1002
- ` 1. Edit your boilerplate files in ${targets.map((t) => `${t}/`).join(", ")}`
1003
- );
1005
+ const editDirs = targets.map((t) => dir ? `${dir}/${t}/` : `${t}/`);
1006
+ console.log(` 1. Edit your boilerplate files in ${editDirs.join(", ")}`);
1004
1007
  console.log(" 2. Push to GitHub");
1005
- console.log(" 3. Publish with: npx updose publish");
1006
1008
  console.log(
1007
- ` 4. Others can install with: npx updose add ${author}/${name}`
1009
+ ` 3. Publish with: npx updose publish${dir ? ` --dir ${dir}` : ""}`
1008
1010
  );
1011
+ if (dir) {
1012
+ console.log(
1013
+ ` 4. Others can install with: npx updose add ${author}/${repoName}/${dir}`
1014
+ );
1015
+ } else {
1016
+ console.log(
1017
+ ` 4. Others can install with: npx updose add ${author}/${name}`
1018
+ );
1019
+ }
1009
1020
  } catch (err) {
1010
1021
  error(
1011
1022
  err instanceof Error ? err.message : "An unexpected error occurred during init."
@@ -1015,7 +1026,7 @@ async function initCommand() {
1015
1026
  }
1016
1027
 
1017
1028
  // src/auth/github-oauth.ts
1018
- var import_promises2 = require("fs/promises");
1029
+ var import_promises3 = require("fs/promises");
1019
1030
  var import_node_os = require("os");
1020
1031
  var import_node_path5 = require("path");
1021
1032
  var import_chalk2 = __toESM(require("chalk"), 1);
@@ -1032,7 +1043,7 @@ var AUTH_FILE_MODE = 384;
1032
1043
  var MAX_POLL_INTERVAL_SEC = 60;
1033
1044
  async function getStoredToken() {
1034
1045
  try {
1035
- const content = await (0, import_promises2.readFile)(AUTH_FILE, "utf-8");
1046
+ const content = await (0, import_promises3.readFile)(AUTH_FILE, "utf-8");
1036
1047
  const data = JSON.parse(content);
1037
1048
  return data.github_token ?? null;
1038
1049
  } catch {
@@ -1041,7 +1052,7 @@ async function getStoredToken() {
1041
1052
  }
1042
1053
  async function getStoredAuth() {
1043
1054
  try {
1044
- const content = await (0, import_promises2.readFile)(AUTH_FILE, "utf-8");
1055
+ const content = await (0, import_promises3.readFile)(AUTH_FILE, "utf-8");
1045
1056
  return JSON.parse(content);
1046
1057
  } catch {
1047
1058
  return null;
@@ -1097,8 +1108,8 @@ async function login() {
1097
1108
  throw new Error("GitHub authorization failed");
1098
1109
  }
1099
1110
  const username = await fetchUsername(token);
1100
- await (0, import_promises2.mkdir)(AUTH_DIR, { recursive: true });
1101
- await (0, import_promises2.writeFile)(
1111
+ await (0, import_promises3.mkdir)(AUTH_DIR, { recursive: true });
1112
+ await (0, import_promises3.writeFile)(
1102
1113
  AUTH_FILE,
1103
1114
  JSON.stringify(
1104
1115
  { github_token: token, github_username: username },
@@ -1183,7 +1194,7 @@ function sleep(ms) {
1183
1194
  }
1184
1195
  async function logout() {
1185
1196
  try {
1186
- await (0, import_promises2.unlink)(AUTH_FILE);
1197
+ await (0, import_promises3.unlink)(AUTH_FILE);
1187
1198
  return true;
1188
1199
  } catch {
1189
1200
  return false;
@@ -1218,20 +1229,32 @@ async function logoutCommand() {
1218
1229
 
1219
1230
  // src/commands/publish.ts
1220
1231
  var import_node_child_process3 = require("child_process");
1221
- var import_promises3 = require("fs/promises");
1232
+ var import_node_fs = require("fs");
1233
+ var import_promises4 = require("fs/promises");
1222
1234
  var import_node_path6 = require("path");
1223
1235
  var import_chalk3 = __toESM(require("chalk"), 1);
1224
1236
  var FETCH_TIMEOUT_MS3 = 1e4;
1225
- async function publishCommand() {
1237
+ async function publishCommand(options) {
1226
1238
  const cwd = process.cwd();
1239
+ const dir = options.dir;
1240
+ const manifestDir = dir ? (0, import_node_path6.join)(cwd, dir) : cwd;
1241
+ if (dir && !(0, import_node_fs.existsSync)(manifestDir)) {
1242
+ error(`Directory "${dir}" does not exist.`);
1243
+ process.exitCode = 1;
1244
+ return;
1245
+ }
1227
1246
  let raw;
1228
1247
  try {
1229
- const content = await (0, import_promises3.readFile)((0, import_node_path6.join)(cwd, MANIFEST_FILENAME), "utf-8");
1248
+ const content = await (0, import_promises4.readFile)(
1249
+ (0, import_node_path6.join)(manifestDir, MANIFEST_FILENAME),
1250
+ "utf-8"
1251
+ );
1230
1252
  raw = JSON.parse(content);
1231
1253
  } catch (err) {
1232
1254
  if (err.code === "ENOENT") {
1255
+ const location = dir ? `"${dir}"` : "current directory";
1233
1256
  error(
1234
- `No ${MANIFEST_FILENAME} found in current directory. Run \`updose init\` first.`
1257
+ `No ${MANIFEST_FILENAME} found in ${location}. Run \`updose init\` first.`
1235
1258
  );
1236
1259
  } else {
1237
1260
  error(
@@ -1265,9 +1288,10 @@ async function publishCommand() {
1265
1288
  process.exitCode = 1;
1266
1289
  return;
1267
1290
  }
1268
- if (manifest.name.toLowerCase() !== repoName.toLowerCase()) {
1291
+ const expectedName = dir ? `${repoName}/${dir}` : repoName;
1292
+ if (manifest.name.toLowerCase() !== expectedName.toLowerCase()) {
1269
1293
  error(
1270
- `Manifest name "${manifest.name}" does not match repository name "${repoName}".`
1294
+ `Manifest name "${manifest.name}" does not match expected name "${expectedName}".`
1271
1295
  );
1272
1296
  process.exitCode = 1;
1273
1297
  return;
@@ -1317,6 +1341,9 @@ Make sure you have pushed your code to GitHub.`
1317
1341
  console.log(` Name: ${manifest.name}`);
1318
1342
  console.log(` Version: ${manifest.version}`);
1319
1343
  console.log(` Repository: ${repo}`);
1344
+ if (dir) {
1345
+ console.log(` Directory: ${dir}`);
1346
+ }
1320
1347
  console.log(` Targets: ${manifest.targets.join(", ")}`);
1321
1348
  if (manifest.tags?.length) {
1322
1349
  console.log(` Tags: ${manifest.tags.join(", ")}`);
@@ -1338,11 +1365,15 @@ Make sure you have pushed your code to GitHub.`
1338
1365
  targets: manifest.targets,
1339
1366
  tags: manifest.tags
1340
1367
  },
1341
- token
1368
+ token,
1369
+ dir
1342
1370
  );
1343
1371
  spinner.success("Published successfully!");
1344
1372
  console.log();
1345
- info(`Users can now install with: ${import_chalk3.default.cyan(`npx updose add ${repo}`)}`);
1373
+ const installPath = dir ? `${repo}/${dir}` : repo;
1374
+ info(
1375
+ `Users can now install with: ${import_chalk3.default.cyan(`npx updose add ${installPath}`)}`
1376
+ );
1346
1377
  } catch (err) {
1347
1378
  spinner.fail("Publication failed");
1348
1379
  error(err.message);
@@ -1378,29 +1409,26 @@ function detectRepo(cwd) {
1378
1409
  var import_chalk4 = __toESM(require("chalk"), 1);
1379
1410
  async function searchCommand(query, options) {
1380
1411
  try {
1381
- if (!query && !options.target && !options.tag && !options.author) {
1382
- error(
1383
- "Please provide a search query or at least one filter (--target, --tag, --author)."
1384
- );
1385
- process.exitCode = 1;
1386
- return;
1387
- }
1388
1412
  const filters = {};
1389
1413
  if (options.target) filters.target = options.target;
1390
1414
  if (options.tag) filters.tag = options.tag;
1391
1415
  if (options.author) filters.author = options.author;
1392
- const results = await searchBoilerplates(query, filters);
1393
- const label = query ? `"${query}"` : "the given filters";
1394
- if (results.length === 0) {
1416
+ const hasParams = !!(query || filters.target || filters.tag || filters.author);
1417
+ const label = query ? `"${query}"` : hasParams ? "the given filters" : "popular boilerplates";
1418
+ const response = await searchBoilerplates(query, filters);
1419
+ if (response.data.length === 0) {
1395
1420
  info(`No boilerplates found for ${label}`);
1396
1421
  return;
1397
1422
  }
1398
1423
  console.log();
1399
- info(`Found ${results.length} result(s) for ${label}:
1424
+ info(`Found ${response.total} result(s) for ${label}:
1400
1425
  `);
1401
- for (const bp of results) {
1426
+ for (const bp of response.data) {
1402
1427
  formatResult(bp);
1403
1428
  }
1429
+ console.log(
1430
+ import_chalk4.default.dim(` Browse more results and details at https://updose.dev/`)
1431
+ );
1404
1432
  } catch (err) {
1405
1433
  error(
1406
1434
  err instanceof Error ? err.message : "An unexpected error occurred during search."
@@ -1414,24 +1442,24 @@ function formatResult(bp) {
1414
1442
  if (bp.description) {
1415
1443
  console.log(` ${bp.description}`);
1416
1444
  }
1417
- const rating = bp.avg_rating > 0 ? `${import_chalk4.default.yellow("\u2605")} ${bp.avg_rating}${bp.rating_count > 0 ? import_chalk4.default.dim(` (${bp.rating_count})`) : ""}` : import_chalk4.default.dim("\u2605 -");
1418
1445
  const downloads = `${import_chalk4.default.green("\u2193")} ${bp.downloads.toLocaleString()}`;
1419
1446
  const targets = import_chalk4.default.cyan(bp.targets.join(", "));
1420
- console.log(` ${rating} ${downloads} ${targets}`);
1447
+ console.log(` ${downloads} ${targets}`);
1421
1448
  if (bp.tags.length > 0) {
1422
1449
  console.log(` ${bp.tags.map((t) => import_chalk4.default.dim(`#${t}`)).join(" ")}`);
1423
1450
  }
1424
- console.log(` ${import_chalk4.default.dim(bp.repo)}`);
1451
+ const repoPath = bp.dir ? `${bp.repo}/${bp.dir}` : bp.repo;
1452
+ console.log(` ${import_chalk4.default.dim(repoPath)}`);
1425
1453
  console.log();
1426
1454
  }
1427
1455
 
1428
1456
  // src/index.ts
1429
1457
  var program = new import_commander.Command();
1430
- program.name("updose").description("AI coding tool boilerplate marketplace").version("0.2.0");
1458
+ program.name("updose").description("AI coding tool boilerplate marketplace").version("0.4.0");
1431
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);
1432
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);
1433
- program.command("init").description("Scaffold a new boilerplate repository").action(initCommand);
1434
- 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);
1435
1463
  program.command("login").description("Log in to GitHub").action(loginCommand);
1436
1464
  program.command("logout").description("Log out from GitHub").action(logoutCommand);
1437
1465
  program.parse();
package/dist/index.js CHANGED
@@ -3,6 +3,9 @@
3
3
  // src/index.ts
4
4
  import { Command } from "commander";
5
5
 
6
+ // src/commands/add.ts
7
+ import { createHash } from "crypto";
8
+
6
9
  // src/constants.ts
7
10
  var USER_AGENT = "updose-cli";
8
11
  var MANIFEST_FILENAME = "updose.json";
@@ -29,18 +32,22 @@ async function searchBoilerplates(query, filters) {
29
32
  }
30
33
  return await res.json();
31
34
  }
32
- async function recordDownload(repo) {
35
+ async function recordDownload(repo, dir, projectHash) {
33
36
  await fetch(`${API_BASE_URL}/download`, {
34
37
  method: "POST",
35
38
  headers: {
36
39
  "Content-Type": "application/json",
37
40
  "User-Agent": USER_AGENT
38
41
  },
39
- body: JSON.stringify({ repo }),
42
+ body: JSON.stringify({
43
+ repo,
44
+ dir: dir ?? null,
45
+ ...projectHash ? { project_hash: projectHash } : {}
46
+ }),
40
47
  signal: AbortSignal.timeout(FETCH_TIMEOUT_MS)
41
48
  });
42
49
  }
43
- async function registerBoilerplate(repo, manifest, githubToken) {
50
+ async function registerBoilerplate(repo, manifest, githubToken, dir) {
44
51
  const res = await fetch(`${API_BASE_URL}/register`, {
45
52
  method: "POST",
46
53
  headers: {
@@ -48,7 +55,7 @@ async function registerBoilerplate(repo, manifest, githubToken) {
48
55
  Authorization: `Bearer ${githubToken}`,
49
56
  "User-Agent": USER_AGENT
50
57
  },
51
- body: JSON.stringify({ repo, manifest }),
58
+ body: JSON.stringify({ repo, manifest, dir: dir ?? null }),
52
59
  signal: AbortSignal.timeout(FETCH_TIMEOUT_MS)
53
60
  });
54
61
  if (!res.ok) {
@@ -286,6 +293,18 @@ function optionalStringArray(obj, key) {
286
293
  // src/core/github.ts
287
294
  var GITHUB_RAW = "https://raw.githubusercontent.com";
288
295
  var FETCH_TIMEOUT_MS2 = 3e4;
296
+ function parseRepoInput(input) {
297
+ const parts = input.split("/");
298
+ if (parts.length < 2 || !parts[0] || !parts[1]) {
299
+ throw new Error(
300
+ `Invalid repository format: "${input}". Expected "owner/repo" or "owner/repo/dir".`
301
+ );
302
+ }
303
+ const repo = `${parts[0]}/${parts[1]}`;
304
+ const raw = parts.length > 2 ? parts.slice(2).join("/").replace(/\/+$/, "") : void 0;
305
+ const dir = raw || void 0;
306
+ return { repo, dir };
307
+ }
289
308
  function parseRepo(repo) {
290
309
  const parts = repo.split("/");
291
310
  if (parts.length !== 2 || !parts[0] || !parts[1]) {
@@ -323,35 +342,10 @@ function handleHttpError(res) {
323
342
  );
324
343
  }
325
344
  }
326
- var branchCache = /* @__PURE__ */ new Map();
327
- async function getDefaultBranch(repo) {
328
- const cached = branchCache.get(repo);
329
- if (cached) return cached;
330
- const { owner, name } = parseRepo(repo);
331
- const res = await fetch(`${GITHUB_API_URL}/repos/${owner}/${name}`, {
332
- headers: {
333
- Accept: GITHUB_ACCEPT_HEADER,
334
- "User-Agent": USER_AGENT,
335
- ...getAuthHeaders()
336
- },
337
- signal: createSignal()
338
- });
339
- if (res.status === 404) {
340
- throw new Error(`Repository not found: ${repo}`);
341
- }
342
- handleHttpError(res);
343
- if (!res.ok) {
344
- throw new Error(`GitHub API error: ${res.status} ${res.statusText}`);
345
- }
346
- const data = await res.json();
347
- branchCache.set(repo, data.default_branch);
348
- return data.default_branch;
349
- }
350
345
  async function fetchFile(repo, path) {
351
346
  const { owner, name } = parseRepo(repo);
352
- const branch = await getDefaultBranch(repo);
353
347
  const encodedPath = path.split("/").map((segment) => encodeURIComponent(segment)).join("/");
354
- const url = `${GITHUB_RAW}/${owner}/${name}/${branch}/${encodedPath}`;
348
+ const url = `${GITHUB_RAW}/${owner}/${name}/HEAD/${encodedPath}`;
355
349
  const res = await fetch(url, {
356
350
  headers: { "User-Agent": USER_AGENT, ...getAuthHeaders() },
357
351
  signal: createSignal()
@@ -365,11 +359,12 @@ async function fetchFile(repo, path) {
365
359
  }
366
360
  return res.text();
367
361
  }
368
- async function fetchManifest(repo) {
369
- const content = await fetchFile(repo, MANIFEST_FILENAME);
362
+ async function fetchManifest(repo, dir) {
363
+ const path = dir ? `${dir}/${MANIFEST_FILENAME}` : MANIFEST_FILENAME;
364
+ const content = await fetchFile(repo, path);
370
365
  if (content === null) {
371
366
  throw new Error(
372
- `No ${MANIFEST_FILENAME} found in ${repo}. Is this an updose boilerplate?`
367
+ `No ${MANIFEST_FILENAME} found in ${dir ? `${repo}/${dir}` : repo}. Is this an updose boilerplate?`
373
368
  );
374
369
  }
375
370
  let raw;
@@ -382,8 +377,7 @@ async function fetchManifest(repo) {
382
377
  }
383
378
  async function fetchRepoTree(repo) {
384
379
  const { owner, name } = parseRepo(repo);
385
- const branch = await getDefaultBranch(repo);
386
- const url = `${GITHUB_API_URL}/repos/${owner}/${name}/git/trees/${branch}?recursive=1`;
380
+ const url = `${GITHUB_API_URL}/repos/${owner}/${name}/git/trees/HEAD?recursive=1`;
387
381
  const res = await fetch(url, {
388
382
  headers: {
389
383
  Accept: GITHUB_ACCEPT_HEADER,
@@ -407,8 +401,9 @@ async function fetchRepoTree(repo) {
407
401
  }
408
402
  return data.tree.filter((entry) => entry.type === "blob");
409
403
  }
410
- async function fetchSkillsJson(repo) {
411
- return fetchFile(repo, SKILLS_FILENAME);
404
+ async function fetchSkillsJson(repo, dir) {
405
+ const path = dir ? `${dir}/${SKILLS_FILENAME}` : SKILLS_FILENAME;
406
+ return fetchFile(repo, path);
412
407
  }
413
408
 
414
409
  // src/core/installer.ts
@@ -591,15 +586,16 @@ async function settledPool(tasks, limit) {
591
586
  );
592
587
  return results;
593
588
  }
594
- async function addCommand(repo, options) {
589
+ async function addCommand(repoInput, options) {
595
590
  try {
591
+ const { repo, dir } = parseRepoInput(repoInput);
596
592
  const cwd = process.cwd();
597
593
  const skipPrompts = options.yes ?? false;
598
594
  const dryRun = options.dryRun ?? false;
599
595
  const manifestSpinner = createSpinner("Fetching updose.json...").start();
600
596
  let manifest;
601
597
  try {
602
- manifest = await fetchManifest(repo);
598
+ manifest = await fetchManifest(repo, dir);
603
599
  manifestSpinner.success(
604
600
  `Found ${manifest.name} by ${manifest.author} (v${manifest.version})`
605
601
  );
@@ -632,7 +628,7 @@ async function addCommand(repo, options) {
632
628
  const filesByTarget = /* @__PURE__ */ new Map();
633
629
  for (const target of selectedTargets) {
634
630
  const sourceDir = getSourceDir(target);
635
- const prefix = `${sourceDir}/`;
631
+ const prefix = dir ? `${dir}/${sourceDir}/` : `${sourceDir}/`;
636
632
  const files = tree.filter((entry) => {
637
633
  if (!entry.path.startsWith(prefix)) return false;
638
634
  const relativePath = entry.path.slice(prefix.length);
@@ -665,7 +661,7 @@ async function addCommand(repo, options) {
665
661
  dryRunCount++;
666
662
  }
667
663
  }
668
- const skillsContent2 = await fetchSkillsJson(repo);
664
+ const skillsContent2 = await fetchSkillsJson(repo, dir);
669
665
  if (skillsContent2 !== null) {
670
666
  try {
671
667
  const skillsManifest = parseSkills(
@@ -724,7 +720,7 @@ async function addCommand(repo, options) {
724
720
  }
725
721
  }
726
722
  let skillsInstalled = 0;
727
- const skillsContent = await fetchSkillsJson(repo);
723
+ const skillsContent = await fetchSkillsJson(repo, dir);
728
724
  if (skillsContent === null) {
729
725
  info("No skills.json found \u2014 skipping skills installation.");
730
726
  } else {
@@ -772,7 +768,8 @@ async function addCommand(repo, options) {
772
768
  const summary = skillsInstalled > 0 ? `${installed} file(s) + ${skillsInstalled} skill(s)` : `${installed} file(s)`;
773
769
  success(`Done! ${summary} installed, ${skipped} skipped.`);
774
770
  if (installed + skillsInstalled > 0) {
775
- await recordDownload(repo).catch(() => {
771
+ const projectHash = createHash("sha256").update(cwd).digest("hex");
772
+ await recordDownload(repo, dir, projectHash).catch(() => {
776
773
  });
777
774
  }
778
775
  } catch (err) {
@@ -785,6 +782,7 @@ async function addCommand(repo, options) {
785
782
 
786
783
  // src/commands/init.ts
787
784
  import { execSync } from "child_process";
785
+ import { mkdir as mkdir2 } from "fs/promises";
788
786
  import { basename, join as join2 } from "path";
789
787
  import prompts2 from "prompts";
790
788
  var DEFAULT_VERSION = "0.1.0";
@@ -890,10 +888,16 @@ function buildFileList(name, description, author, targets) {
890
888
  }
891
889
  return files;
892
890
  }
893
- async function initCommand() {
891
+ async function initCommand(options) {
894
892
  try {
895
893
  const cwd = process.cwd();
896
- const defaultName = basename(cwd);
894
+ const dir = options.dir;
895
+ const baseDir = dir ? join2(cwd, dir) : cwd;
896
+ if (dir) {
897
+ await mkdir2(baseDir, { recursive: true });
898
+ }
899
+ const repoName = basename(cwd);
900
+ const defaultName = dir ? `${repoName}/${dir}` : repoName;
897
901
  const gitUser = getGitHubUsername();
898
902
  info("Scaffolding a new updose boilerplate...\n");
899
903
  let cancelled = false;
@@ -950,7 +954,7 @@ async function initCommand() {
950
954
  let created = 0;
951
955
  let skipped = 0;
952
956
  for (const file of files) {
953
- const destPath = join2(cwd, file.path);
957
+ const destPath = join2(baseDir, file.path);
954
958
  const exists = await fileExists(destPath);
955
959
  if (exists) {
956
960
  const { action } = await prompts2({
@@ -976,14 +980,21 @@ async function initCommand() {
976
980
  success(`Boilerplate scaffolded! (${created} created, ${skipped} skipped)`);
977
981
  console.log();
978
982
  info("Next steps:");
979
- console.log(
980
- ` 1. Edit your boilerplate files in ${targets.map((t) => `${t}/`).join(", ")}`
981
- );
983
+ const editDirs = targets.map((t) => dir ? `${dir}/${t}/` : `${t}/`);
984
+ console.log(` 1. Edit your boilerplate files in ${editDirs.join(", ")}`);
982
985
  console.log(" 2. Push to GitHub");
983
- console.log(" 3. Publish with: npx updose publish");
984
986
  console.log(
985
- ` 4. Others can install with: npx updose add ${author}/${name}`
987
+ ` 3. Publish with: npx updose publish${dir ? ` --dir ${dir}` : ""}`
986
988
  );
989
+ if (dir) {
990
+ console.log(
991
+ ` 4. Others can install with: npx updose add ${author}/${repoName}/${dir}`
992
+ );
993
+ } else {
994
+ console.log(
995
+ ` 4. Others can install with: npx updose add ${author}/${name}`
996
+ );
997
+ }
987
998
  } catch (err) {
988
999
  error(
989
1000
  err instanceof Error ? err.message : "An unexpected error occurred during init."
@@ -993,7 +1004,7 @@ async function initCommand() {
993
1004
  }
994
1005
 
995
1006
  // src/auth/github-oauth.ts
996
- import { mkdir as mkdir2, readFile as readFile2, unlink, writeFile as writeFile2 } from "fs/promises";
1007
+ import { mkdir as mkdir3, readFile as readFile2, unlink, writeFile as writeFile2 } from "fs/promises";
997
1008
  import { homedir } from "os";
998
1009
  import { join as join3 } from "path";
999
1010
  import chalk2 from "chalk";
@@ -1075,7 +1086,7 @@ async function login() {
1075
1086
  throw new Error("GitHub authorization failed");
1076
1087
  }
1077
1088
  const username = await fetchUsername(token);
1078
- await mkdir2(AUTH_DIR, { recursive: true });
1089
+ await mkdir3(AUTH_DIR, { recursive: true });
1079
1090
  await writeFile2(
1080
1091
  AUTH_FILE,
1081
1092
  JSON.stringify(
@@ -1196,20 +1207,32 @@ async function logoutCommand() {
1196
1207
 
1197
1208
  // src/commands/publish.ts
1198
1209
  import { execSync as execSync2 } from "child_process";
1210
+ import { existsSync } from "fs";
1199
1211
  import { readFile as readFile3 } from "fs/promises";
1200
1212
  import { join as join4 } from "path";
1201
1213
  import chalk3 from "chalk";
1202
1214
  var FETCH_TIMEOUT_MS3 = 1e4;
1203
- async function publishCommand() {
1215
+ async function publishCommand(options) {
1204
1216
  const cwd = process.cwd();
1217
+ const dir = options.dir;
1218
+ const manifestDir = dir ? join4(cwd, dir) : cwd;
1219
+ if (dir && !existsSync(manifestDir)) {
1220
+ error(`Directory "${dir}" does not exist.`);
1221
+ process.exitCode = 1;
1222
+ return;
1223
+ }
1205
1224
  let raw;
1206
1225
  try {
1207
- const content = await readFile3(join4(cwd, MANIFEST_FILENAME), "utf-8");
1226
+ const content = await readFile3(
1227
+ join4(manifestDir, MANIFEST_FILENAME),
1228
+ "utf-8"
1229
+ );
1208
1230
  raw = JSON.parse(content);
1209
1231
  } catch (err) {
1210
1232
  if (err.code === "ENOENT") {
1233
+ const location = dir ? `"${dir}"` : "current directory";
1211
1234
  error(
1212
- `No ${MANIFEST_FILENAME} found in current directory. Run \`updose init\` first.`
1235
+ `No ${MANIFEST_FILENAME} found in ${location}. Run \`updose init\` first.`
1213
1236
  );
1214
1237
  } else {
1215
1238
  error(
@@ -1243,9 +1266,10 @@ async function publishCommand() {
1243
1266
  process.exitCode = 1;
1244
1267
  return;
1245
1268
  }
1246
- if (manifest.name.toLowerCase() !== repoName.toLowerCase()) {
1269
+ const expectedName = dir ? `${repoName}/${dir}` : repoName;
1270
+ if (manifest.name.toLowerCase() !== expectedName.toLowerCase()) {
1247
1271
  error(
1248
- `Manifest name "${manifest.name}" does not match repository name "${repoName}".`
1272
+ `Manifest name "${manifest.name}" does not match expected name "${expectedName}".`
1249
1273
  );
1250
1274
  process.exitCode = 1;
1251
1275
  return;
@@ -1295,6 +1319,9 @@ Make sure you have pushed your code to GitHub.`
1295
1319
  console.log(` Name: ${manifest.name}`);
1296
1320
  console.log(` Version: ${manifest.version}`);
1297
1321
  console.log(` Repository: ${repo}`);
1322
+ if (dir) {
1323
+ console.log(` Directory: ${dir}`);
1324
+ }
1298
1325
  console.log(` Targets: ${manifest.targets.join(", ")}`);
1299
1326
  if (manifest.tags?.length) {
1300
1327
  console.log(` Tags: ${manifest.tags.join(", ")}`);
@@ -1316,11 +1343,15 @@ Make sure you have pushed your code to GitHub.`
1316
1343
  targets: manifest.targets,
1317
1344
  tags: manifest.tags
1318
1345
  },
1319
- token
1346
+ token,
1347
+ dir
1320
1348
  );
1321
1349
  spinner.success("Published successfully!");
1322
1350
  console.log();
1323
- info(`Users can now install with: ${chalk3.cyan(`npx updose add ${repo}`)}`);
1351
+ const installPath = dir ? `${repo}/${dir}` : repo;
1352
+ info(
1353
+ `Users can now install with: ${chalk3.cyan(`npx updose add ${installPath}`)}`
1354
+ );
1324
1355
  } catch (err) {
1325
1356
  spinner.fail("Publication failed");
1326
1357
  error(err.message);
@@ -1356,29 +1387,26 @@ function detectRepo(cwd) {
1356
1387
  import chalk4 from "chalk";
1357
1388
  async function searchCommand(query, options) {
1358
1389
  try {
1359
- if (!query && !options.target && !options.tag && !options.author) {
1360
- error(
1361
- "Please provide a search query or at least one filter (--target, --tag, --author)."
1362
- );
1363
- process.exitCode = 1;
1364
- return;
1365
- }
1366
1390
  const filters = {};
1367
1391
  if (options.target) filters.target = options.target;
1368
1392
  if (options.tag) filters.tag = options.tag;
1369
1393
  if (options.author) filters.author = options.author;
1370
- const results = await searchBoilerplates(query, filters);
1371
- const label = query ? `"${query}"` : "the given filters";
1372
- if (results.length === 0) {
1394
+ const hasParams = !!(query || filters.target || filters.tag || filters.author);
1395
+ const label = query ? `"${query}"` : hasParams ? "the given filters" : "popular boilerplates";
1396
+ const response = await searchBoilerplates(query, filters);
1397
+ if (response.data.length === 0) {
1373
1398
  info(`No boilerplates found for ${label}`);
1374
1399
  return;
1375
1400
  }
1376
1401
  console.log();
1377
- info(`Found ${results.length} result(s) for ${label}:
1402
+ info(`Found ${response.total} result(s) for ${label}:
1378
1403
  `);
1379
- for (const bp of results) {
1404
+ for (const bp of response.data) {
1380
1405
  formatResult(bp);
1381
1406
  }
1407
+ console.log(
1408
+ chalk4.dim(` Browse more results and details at https://updose.dev/`)
1409
+ );
1382
1410
  } catch (err) {
1383
1411
  error(
1384
1412
  err instanceof Error ? err.message : "An unexpected error occurred during search."
@@ -1392,24 +1420,24 @@ function formatResult(bp) {
1392
1420
  if (bp.description) {
1393
1421
  console.log(` ${bp.description}`);
1394
1422
  }
1395
- const rating = bp.avg_rating > 0 ? `${chalk4.yellow("\u2605")} ${bp.avg_rating}${bp.rating_count > 0 ? chalk4.dim(` (${bp.rating_count})`) : ""}` : chalk4.dim("\u2605 -");
1396
1423
  const downloads = `${chalk4.green("\u2193")} ${bp.downloads.toLocaleString()}`;
1397
1424
  const targets = chalk4.cyan(bp.targets.join(", "));
1398
- console.log(` ${rating} ${downloads} ${targets}`);
1425
+ console.log(` ${downloads} ${targets}`);
1399
1426
  if (bp.tags.length > 0) {
1400
1427
  console.log(` ${bp.tags.map((t) => chalk4.dim(`#${t}`)).join(" ")}`);
1401
1428
  }
1402
- console.log(` ${chalk4.dim(bp.repo)}`);
1429
+ const repoPath = bp.dir ? `${bp.repo}/${bp.dir}` : bp.repo;
1430
+ console.log(` ${chalk4.dim(repoPath)}`);
1403
1431
  console.log();
1404
1432
  }
1405
1433
 
1406
1434
  // src/index.ts
1407
1435
  var program = new Command();
1408
- program.name("updose").description("AI coding tool boilerplate marketplace").version("0.2.0");
1436
+ program.name("updose").description("AI coding tool boilerplate marketplace").version("0.4.0");
1409
1437
  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);
1410
1438
  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);
1411
- program.command("init").description("Scaffold a new boilerplate repository").action(initCommand);
1412
- program.command("publish").description("Publish your boilerplate to the registry").action(publishCommand);
1439
+ program.command("init").description("Scaffold a new boilerplate repository").option("--dir <dir>", "Create boilerplate in a subdirectory").action(initCommand);
1440
+ program.command("publish").description("Publish your boilerplate to the registry").option("--dir <dir>", "Publish from a subdirectory").action(publishCommand);
1413
1441
  program.command("login").description("Log in to GitHub").action(loginCommand);
1414
1442
  program.command("logout").description("Log out from GitHub").action(logoutCommand);
1415
1443
  program.parse();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "updose",
3
- "version": "0.2.0",
3
+ "version": "0.4.0",
4
4
  "type": "module",
5
5
  "description": "AI coding tool boilerplate marketplace",
6
6
  "main": "dist/index.cjs",