updose 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +177 -21
- package/dist/index.cjs +91 -41
- package/dist/index.js +86 -36
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -31,6 +31,7 @@ npx updose <command>
|
|
|
31
31
|
- [`skills.json` Reference](#skillsjson-reference)
|
|
32
32
|
- [Example: Single-Target Boilerplate (Claude)](#example-single-target-boilerplate-claude)
|
|
33
33
|
- [Example: Multi-Target Boilerplate](#example-multi-target-boilerplate)
|
|
34
|
+
- [Monorepo Support](#monorepo-support)
|
|
34
35
|
- [Publishing](#publishing)
|
|
35
36
|
- [License](#license)
|
|
36
37
|
|
|
@@ -43,6 +44,8 @@ npx updose search react
|
|
|
43
44
|
# Install a boilerplate from a GitHub repository
|
|
44
45
|
npx updose add owner/repo-name
|
|
45
46
|
|
|
47
|
+
# Install from a subdirectory within a monorepo
|
|
48
|
+
npx updose add owner/repo-name/nextjs
|
|
46
49
|
```
|
|
47
50
|
|
|
48
51
|
## Commands
|
|
@@ -51,21 +54,29 @@ npx updose add owner/repo-name
|
|
|
51
54
|
|
|
52
55
|
Install a boilerplate from a GitHub repository into your project.
|
|
53
56
|
|
|
57
|
+
The `<repo>` argument accepts two formats:
|
|
58
|
+
|
|
59
|
+
- **`owner/repo`** — installs from the repository root (standard boilerplate)
|
|
60
|
+
- **`owner/repo/dir`** — installs from a subdirectory within the repository (monorepo boilerplate). The `dir` can be nested (e.g., `owner/repo/templates/v2`).
|
|
61
|
+
|
|
54
62
|
```bash
|
|
55
|
-
npx updose add owner/repo-name
|
|
56
|
-
npx updose add owner/repo-name
|
|
57
|
-
npx updose add owner/repo-name
|
|
63
|
+
npx updose add owner/repo-name # Install from repository root
|
|
64
|
+
npx updose add owner/repo-name/nextjs # Install from "nextjs" subdirectory
|
|
65
|
+
npx updose add owner/repo-name/templates/v2 # Install from nested subdirectory
|
|
66
|
+
npx updose add owner/repo-name -y # Skip all prompts
|
|
67
|
+
npx updose add owner/repo-name/nextjs --dry-run # Preview monorepo install
|
|
58
68
|
```
|
|
59
69
|
|
|
60
70
|
**What happens when you run `add`:**
|
|
61
71
|
|
|
62
|
-
1.
|
|
63
|
-
2.
|
|
64
|
-
3.
|
|
65
|
-
4.
|
|
72
|
+
1. Parses the `<repo>` argument to determine the repository (`owner/repo`) and optional subdirectory
|
|
73
|
+
2. Fetches the boilerplate's `updose.json` manifest from the repository root or the specified subdirectory
|
|
74
|
+
3. If the boilerplate supports multiple targets (e.g., both Claude and Gemini), prompts you to choose which targets to install. With `-y`, all targets are installed automatically.
|
|
75
|
+
4. Downloads the file tree from the repository and filters files for the selected target(s). When a subdirectory is specified, only files under that subdirectory are considered.
|
|
76
|
+
5. Installs each file into your project. If a file already exists, the prompt depends on the file type:
|
|
66
77
|
- **Main docs** (`CLAUDE.md`, `AGENTS.md`, `GEMINI.md`): **Append** / **Overwrite** / **Skip**
|
|
67
78
|
- **Other files** (rules, commands, agents, etc.): **Overwrite** / **Skip**
|
|
68
|
-
|
|
79
|
+
6. If a `skills.json` file exists in the boilerplate (or its subdirectory), installs each declared skill via [skills.sh](https://skills.sh/). Skills are installed for the selected targets (`-a`), copied into the project (`--copy`), and auto-confirmed (`-y`)
|
|
69
80
|
|
|
70
81
|
| Option | Description |
|
|
71
82
|
|-------- |------------- |
|
|
@@ -138,6 +149,17 @@ mkdir my-boilerplate && cd my-boilerplate
|
|
|
138
149
|
npx updose init
|
|
139
150
|
```
|
|
140
151
|
|
|
152
|
+
To scaffold inside a subdirectory (for monorepo setups), use the `--dir` option:
|
|
153
|
+
|
|
154
|
+
```bash
|
|
155
|
+
npx updose init --dir nextjs # Creates boilerplate in the "nextjs" subdirectory
|
|
156
|
+
npx updose init --dir templates/v2 # Nested subdirectory is also supported
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
| Option | Description |
|
|
160
|
+
|-------- |------------- |
|
|
161
|
+
| `--dir <dir>` | Create the boilerplate inside the specified subdirectory instead of the repository root. The directory is created if it doesn't exist. |
|
|
162
|
+
|
|
141
163
|
**What happens when you run `init`:**
|
|
142
164
|
|
|
143
165
|
1. Prompts you for boilerplate configuration (see below)
|
|
@@ -145,11 +167,13 @@ npx updose init
|
|
|
145
167
|
3. If a file already exists, asks whether to **Overwrite** or **Skip**
|
|
146
168
|
4. Shows next steps for publishing
|
|
147
169
|
|
|
170
|
+
When `--dir` is used, all generated files are placed inside the specified subdirectory instead of the repository root.
|
|
171
|
+
|
|
148
172
|
**Interactive prompts:**
|
|
149
173
|
|
|
150
174
|
| Prompt | Description | Default |
|
|
151
175
|
|-------- |------------- |--------- |
|
|
152
|
-
| **Name** | Boilerplate name |
|
|
176
|
+
| **Name** | Boilerplate name | Without `--dir`: current directory name (e.g., `my-boilerplate`). With `--dir`: `<repo>/<dir>` (e.g., `my-boilerplate/nextjs`) |
|
|
153
177
|
| **Description** | Short description (optional) | — |
|
|
154
178
|
| **Author** | GitHub username | Auto-detected from `git config github.user` or `gh api user`. If neither is available, you enter it manually. |
|
|
155
179
|
| **Targets** | Which AI tools to support (multiselect) | All selected (`claude`, `codex`, `gemini`) |
|
|
@@ -172,7 +196,7 @@ Plus target-specific directories based on your selection:
|
|
|
172
196
|
| Codex | `codex/AGENTS.md` |
|
|
173
197
|
| Gemini | `gemini/GEMINI.md`, `gemini/skills/` |
|
|
174
198
|
|
|
175
|
-
**Example — scaffolding with all targets selected:**
|
|
199
|
+
**Example — scaffolding with all targets selected (no `--dir`):**
|
|
176
200
|
|
|
177
201
|
```
|
|
178
202
|
my-boilerplate/
|
|
@@ -191,27 +215,57 @@ my-boilerplate/
|
|
|
191
215
|
└── skills/
|
|
192
216
|
```
|
|
193
217
|
|
|
218
|
+
**Example — scaffolding with `--dir nextjs`:**
|
|
219
|
+
|
|
220
|
+
```
|
|
221
|
+
my-monorepo/
|
|
222
|
+
├── nextjs/ ← created by --dir
|
|
223
|
+
│ ├── updose.json
|
|
224
|
+
│ ├── skills.json
|
|
225
|
+
│ ├── README.md
|
|
226
|
+
│ ├── claude/
|
|
227
|
+
│ │ ├── CLAUDE.md
|
|
228
|
+
│ │ ├── rules/
|
|
229
|
+
│ │ ├── agents/
|
|
230
|
+
│ │ └── skills/
|
|
231
|
+
│ ├── codex/
|
|
232
|
+
│ │ └── AGENTS.md
|
|
233
|
+
│ └── gemini/
|
|
234
|
+
│ ├── GEMINI.md
|
|
235
|
+
│ └── skills/
|
|
236
|
+
├── remix/ ← another boilerplate in the same repo
|
|
237
|
+
│ └── ...
|
|
238
|
+
└── README.md ← repo-level README (not managed by updose)
|
|
239
|
+
```
|
|
240
|
+
|
|
194
241
|
After scaffolding, follow the next steps printed by the command:
|
|
195
242
|
|
|
196
243
|
1. Edit your boilerplate files in each target directory
|
|
197
244
|
2. Push to GitHub
|
|
198
|
-
3. Publish with `npx updose publish`
|
|
199
|
-
4. Others can install with `npx updose add <author>/<name>`
|
|
245
|
+
3. Publish with `npx updose publish` (or `npx updose publish --dir nextjs` for monorepo)
|
|
246
|
+
4. Others can install with `npx updose add <author>/<name>` (or `npx updose add <author>/<repo>/nextjs` for monorepo)
|
|
200
247
|
|
|
201
248
|
### `updose publish`
|
|
202
249
|
|
|
203
250
|
Publish your boilerplate to the marketplace so others can find and install it.
|
|
204
251
|
|
|
205
252
|
```bash
|
|
206
|
-
npx updose publish
|
|
253
|
+
npx updose publish # Publish from repository root
|
|
254
|
+
npx updose publish --dir nextjs # Publish from a subdirectory (monorepo)
|
|
207
255
|
```
|
|
208
256
|
|
|
257
|
+
| Option | Description |
|
|
258
|
+
|-------- |------------- |
|
|
259
|
+
| `--dir <dir>` | Read `updose.json` from the specified subdirectory instead of the repository root. Use this when publishing a monorepo boilerplate. |
|
|
260
|
+
|
|
209
261
|
**What happens when you run `publish`:**
|
|
210
262
|
|
|
211
|
-
1. Reads and parses `updose.json` from the current directory. If the file is missing, shows an error and suggests running `updose init` first.
|
|
263
|
+
1. Reads and parses `updose.json` from the current directory (or the subdirectory specified by `--dir`). If the file is missing, shows an error and suggests running `updose init` first.
|
|
212
264
|
2. Validates the manifest structure (name, author, version, targets are required)
|
|
213
265
|
3. Detects the GitHub repository by running `git remote get-url origin`. Supports both HTTPS (`https://github.com/owner/repo.git`) and SSH (`git@github.com:owner/repo.git`) formats.
|
|
214
|
-
4. Validates that `author` and `name` in `updose.json` match the
|
|
266
|
+
4. Validates that `author` and `name` in `updose.json` match the expected name:
|
|
267
|
+
- Without `--dir`: `name` must match the repository name (e.g., `react-starter`)
|
|
268
|
+
- With `--dir`: `name` must match `<repo>/<dir>` (e.g., `my-monorepo/nextjs`)
|
|
215
269
|
5. Authenticates via GitHub — if no valid token exists, automatically runs the `login` flow (see below)
|
|
216
270
|
6. Verifies the repository actually exists on GitHub. If the repo has not been pushed yet, shows an error and exits.
|
|
217
271
|
7. Displays a publication summary for review:
|
|
@@ -225,13 +279,27 @@ Publishing:
|
|
|
225
279
|
Tags: react, typescript, web
|
|
226
280
|
```
|
|
227
281
|
|
|
282
|
+
When `--dir` is used, the summary also includes a `Directory` field:
|
|
283
|
+
|
|
284
|
+
```
|
|
285
|
+
Publishing:
|
|
286
|
+
Name: my-monorepo/nextjs
|
|
287
|
+
Version: 1.0.0
|
|
288
|
+
Repository: example-user/my-monorepo
|
|
289
|
+
Directory: nextjs
|
|
290
|
+
Targets: claude
|
|
291
|
+
Tags: react, nextjs
|
|
292
|
+
```
|
|
293
|
+
|
|
228
294
|
8. Asks for confirmation: **"Publish to registry?"** (defaults to yes). If declined, displays "Publish cancelled." and exits.
|
|
229
295
|
9. Registers the boilerplate in the marketplace registry
|
|
230
|
-
10. On success, displays
|
|
296
|
+
10. On success, displays the install command:
|
|
297
|
+
- Without `--dir`: `Users can now install with: npx updose add owner/repo`
|
|
298
|
+
- With `--dir`: `Users can now install with: npx updose add owner/repo/dir`
|
|
231
299
|
|
|
232
300
|
**Prerequisites:**
|
|
233
301
|
|
|
234
|
-
- A valid `updose.json` in the current directory (run `updose init` to create one)
|
|
302
|
+
- A valid `updose.json` in the current directory or specified subdirectory (run `updose init` to create one)
|
|
235
303
|
- A GitHub remote (`origin`) configured and pushed to GitHub
|
|
236
304
|
- GitHub authentication (handled automatically if not already logged in)
|
|
237
305
|
|
|
@@ -388,7 +456,7 @@ The manifest file that describes your boilerplate.
|
|
|
388
456
|
|
|
389
457
|
| Field | Required | Description |
|
|
390
458
|
|------- |---------- |------------- |
|
|
391
|
-
| `name` | Yes | The boilerplate name. Must match the GitHub repository name.
|
|
459
|
+
| `name` | Yes | The boilerplate name. Must match the GitHub repository name (e.g., `react-starter`). For monorepo boilerplates, must be `<repo>/<dir>` (e.g., `my-starters/nextjs`). |
|
|
392
460
|
| `author` | Yes | Author name. Must match the GitHub repository owner. |
|
|
393
461
|
| `version` | Yes | Version string following [semver](https://semver.org/) (e.g., `1.0.0`). |
|
|
394
462
|
| `targets` | Yes | Array of supported targets: `"claude"`, `"codex"`, and/or `"gemini"`. |
|
|
@@ -497,16 +565,104 @@ boilerplate-multi-target/
|
|
|
497
565
|
|
|
498
566
|
When a user installs a multi-target boilerplate, they are prompted to choose which targets to install. With `-y`, all targets are installed automatically.
|
|
499
567
|
|
|
568
|
+
### Monorepo Support
|
|
569
|
+
|
|
570
|
+
A single GitHub repository can contain multiple boilerplates, each in its own subdirectory. This is useful when you want to publish several related boilerplates (e.g., framework-specific starters) from one repo.
|
|
571
|
+
|
|
572
|
+
**How it works:**
|
|
573
|
+
|
|
574
|
+
- Each subdirectory is an independent boilerplate with its own `updose.json`, `skills.json`, and target directories
|
|
575
|
+
- The `name` field in `updose.json` must be `<repo>/<dir>` (e.g., `my-starters/nextjs`)
|
|
576
|
+
- Users install with `npx updose add owner/repo/dir` instead of `npx updose add owner/repo`
|
|
577
|
+
|
|
578
|
+
**Monorepo directory structure:**
|
|
579
|
+
|
|
580
|
+
```
|
|
581
|
+
my-starters/ ← GitHub repository root
|
|
582
|
+
├── README.md ← repo-level README (not managed by updose)
|
|
583
|
+
├── nextjs/ ← boilerplate for Next.js
|
|
584
|
+
│ ├── updose.json ← name: "my-starters/nextjs"
|
|
585
|
+
│ ├── skills.json
|
|
586
|
+
│ ├── claude/
|
|
587
|
+
│ │ ├── CLAUDE.md
|
|
588
|
+
│ │ └── rules/
|
|
589
|
+
│ │ └── nextjs-conventions.md
|
|
590
|
+
│ └── gemini/
|
|
591
|
+
│ └── GEMINI.md
|
|
592
|
+
├── remix/ ← boilerplate for Remix
|
|
593
|
+
│ ├── updose.json ← name: "my-starters/remix"
|
|
594
|
+
│ ├── skills.json
|
|
595
|
+
│ └── claude/
|
|
596
|
+
│ ├── CLAUDE.md
|
|
597
|
+
│ └── rules/
|
|
598
|
+
│ └── remix-conventions.md
|
|
599
|
+
└── sveltekit/ ← boilerplate for SvelteKit
|
|
600
|
+
├── updose.json ← name: "my-starters/sveltekit"
|
|
601
|
+
├── skills.json
|
|
602
|
+
└── claude/
|
|
603
|
+
└── CLAUDE.md
|
|
604
|
+
```
|
|
605
|
+
|
|
606
|
+
**`updose.json` for a monorepo boilerplate (`nextjs/updose.json`):**
|
|
607
|
+
|
|
608
|
+
```json
|
|
609
|
+
{
|
|
610
|
+
"name": "my-starters/nextjs",
|
|
611
|
+
"author": "example-user",
|
|
612
|
+
"version": "1.0.0",
|
|
613
|
+
"description": "Next.js boilerplate for Claude and Gemini",
|
|
614
|
+
"targets": ["claude", "gemini"],
|
|
615
|
+
"tags": ["nextjs", "react", "web"]
|
|
616
|
+
}
|
|
617
|
+
```
|
|
618
|
+
|
|
619
|
+
> Note: The `name` field uses the format `<repo>/<dir>` (e.g., `my-starters/nextjs`), not just the directory name.
|
|
620
|
+
|
|
621
|
+
**Workflow for creating a monorepo boilerplate:**
|
|
622
|
+
|
|
623
|
+
```bash
|
|
624
|
+
# 1. Scaffold each boilerplate in its own subdirectory
|
|
625
|
+
npx updose init --dir nextjs
|
|
626
|
+
npx updose init --dir remix
|
|
627
|
+
npx updose init --dir sveltekit
|
|
628
|
+
|
|
629
|
+
# 2. Edit each boilerplate's files
|
|
630
|
+
# (edit nextjs/claude/CLAUDE.md, remix/claude/CLAUDE.md, etc.)
|
|
631
|
+
|
|
632
|
+
# 3. Push to GitHub
|
|
633
|
+
git add . && git commit -m "Add boilerplates" && git push
|
|
634
|
+
|
|
635
|
+
# 4. Publish each boilerplate separately
|
|
636
|
+
npx updose publish --dir nextjs
|
|
637
|
+
npx updose publish --dir remix
|
|
638
|
+
npx updose publish --dir sveltekit
|
|
639
|
+
```
|
|
640
|
+
|
|
641
|
+
**Users install each boilerplate independently:**
|
|
642
|
+
|
|
643
|
+
```bash
|
|
644
|
+
npx updose add example-user/my-starters/nextjs
|
|
645
|
+
npx updose add example-user/my-starters/remix
|
|
646
|
+
```
|
|
647
|
+
|
|
500
648
|
## Publishing
|
|
501
649
|
|
|
502
650
|
To share your boilerplate with others through the marketplace:
|
|
503
651
|
|
|
504
|
-
1. Scaffold with `npx updose init`
|
|
652
|
+
1. Scaffold with `npx updose init` (or `npx updose init --dir <dir>` for monorepo)
|
|
505
653
|
2. Add your content (rules, commands, agents, skills, etc.)
|
|
506
654
|
3. Push to a GitHub repository
|
|
507
|
-
4. Run `npx updose publish`
|
|
655
|
+
4. Run `npx updose publish` (or `npx updose publish --dir <dir>` for monorepo)
|
|
656
|
+
|
|
657
|
+
After publishing, anyone can install your boilerplate:
|
|
508
658
|
|
|
509
|
-
|
|
659
|
+
```bash
|
|
660
|
+
# Standard boilerplate
|
|
661
|
+
npx updose add your-username/my-boilerplate
|
|
662
|
+
|
|
663
|
+
# Monorepo boilerplate
|
|
664
|
+
npx updose add your-username/my-monorepo/nextjs
|
|
665
|
+
```
|
|
510
666
|
|
|
511
667
|
## License
|
|
512
668
|
|
package/dist/index.cjs
CHANGED
|
@@ -51,18 +51,18 @@ async function searchBoilerplates(query, filters) {
|
|
|
51
51
|
}
|
|
52
52
|
return await res.json();
|
|
53
53
|
}
|
|
54
|
-
async function recordDownload(repo) {
|
|
54
|
+
async function recordDownload(repo, dir) {
|
|
55
55
|
await fetch(`${API_BASE_URL}/download`, {
|
|
56
56
|
method: "POST",
|
|
57
57
|
headers: {
|
|
58
58
|
"Content-Type": "application/json",
|
|
59
59
|
"User-Agent": USER_AGENT
|
|
60
60
|
},
|
|
61
|
-
body: JSON.stringify({ repo }),
|
|
61
|
+
body: JSON.stringify({ repo, dir: dir ?? null }),
|
|
62
62
|
signal: AbortSignal.timeout(FETCH_TIMEOUT_MS)
|
|
63
63
|
});
|
|
64
64
|
}
|
|
65
|
-
async function registerBoilerplate(repo, manifest, githubToken) {
|
|
65
|
+
async function registerBoilerplate(repo, manifest, githubToken, dir) {
|
|
66
66
|
const res = await fetch(`${API_BASE_URL}/register`, {
|
|
67
67
|
method: "POST",
|
|
68
68
|
headers: {
|
|
@@ -70,7 +70,7 @@ async function registerBoilerplate(repo, manifest, githubToken) {
|
|
|
70
70
|
Authorization: `Bearer ${githubToken}`,
|
|
71
71
|
"User-Agent": USER_AGENT
|
|
72
72
|
},
|
|
73
|
-
body: JSON.stringify({ repo, manifest }),
|
|
73
|
+
body: JSON.stringify({ repo, manifest, dir: dir ?? null }),
|
|
74
74
|
signal: AbortSignal.timeout(FETCH_TIMEOUT_MS)
|
|
75
75
|
});
|
|
76
76
|
if (!res.ok) {
|
|
@@ -308,6 +308,18 @@ function optionalStringArray(obj, key) {
|
|
|
308
308
|
// src/core/github.ts
|
|
309
309
|
var GITHUB_RAW = "https://raw.githubusercontent.com";
|
|
310
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
|
+
}
|
|
311
323
|
function parseRepo(repo) {
|
|
312
324
|
const parts = repo.split("/");
|
|
313
325
|
if (parts.length !== 2 || !parts[0] || !parts[1]) {
|
|
@@ -387,11 +399,12 @@ async function fetchFile(repo, path) {
|
|
|
387
399
|
}
|
|
388
400
|
return res.text();
|
|
389
401
|
}
|
|
390
|
-
async function fetchManifest(repo) {
|
|
391
|
-
const
|
|
402
|
+
async function fetchManifest(repo, dir) {
|
|
403
|
+
const path = dir ? `${dir}/${MANIFEST_FILENAME}` : MANIFEST_FILENAME;
|
|
404
|
+
const content = await fetchFile(repo, path);
|
|
392
405
|
if (content === null) {
|
|
393
406
|
throw new Error(
|
|
394
|
-
`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?`
|
|
395
408
|
);
|
|
396
409
|
}
|
|
397
410
|
let raw;
|
|
@@ -429,8 +442,9 @@ async function fetchRepoTree(repo) {
|
|
|
429
442
|
}
|
|
430
443
|
return data.tree.filter((entry) => entry.type === "blob");
|
|
431
444
|
}
|
|
432
|
-
async function fetchSkillsJson(repo) {
|
|
433
|
-
|
|
445
|
+
async function fetchSkillsJson(repo, dir) {
|
|
446
|
+
const path = dir ? `${dir}/${SKILLS_FILENAME}` : SKILLS_FILENAME;
|
|
447
|
+
return fetchFile(repo, path);
|
|
434
448
|
}
|
|
435
449
|
|
|
436
450
|
// src/core/installer.ts
|
|
@@ -613,15 +627,16 @@ async function settledPool(tasks, limit) {
|
|
|
613
627
|
);
|
|
614
628
|
return results;
|
|
615
629
|
}
|
|
616
|
-
async function addCommand(
|
|
630
|
+
async function addCommand(repoInput, options) {
|
|
617
631
|
try {
|
|
632
|
+
const { repo, dir } = parseRepoInput(repoInput);
|
|
618
633
|
const cwd = process.cwd();
|
|
619
634
|
const skipPrompts = options.yes ?? false;
|
|
620
635
|
const dryRun = options.dryRun ?? false;
|
|
621
636
|
const manifestSpinner = createSpinner("Fetching updose.json...").start();
|
|
622
637
|
let manifest;
|
|
623
638
|
try {
|
|
624
|
-
manifest = await fetchManifest(repo);
|
|
639
|
+
manifest = await fetchManifest(repo, dir);
|
|
625
640
|
manifestSpinner.success(
|
|
626
641
|
`Found ${manifest.name} by ${manifest.author} (v${manifest.version})`
|
|
627
642
|
);
|
|
@@ -654,7 +669,7 @@ async function addCommand(repo, options) {
|
|
|
654
669
|
const filesByTarget = /* @__PURE__ */ new Map();
|
|
655
670
|
for (const target of selectedTargets) {
|
|
656
671
|
const sourceDir = getSourceDir(target);
|
|
657
|
-
const prefix = `${sourceDir}/`;
|
|
672
|
+
const prefix = dir ? `${dir}/${sourceDir}/` : `${sourceDir}/`;
|
|
658
673
|
const files = tree.filter((entry) => {
|
|
659
674
|
if (!entry.path.startsWith(prefix)) return false;
|
|
660
675
|
const relativePath = entry.path.slice(prefix.length);
|
|
@@ -687,7 +702,7 @@ async function addCommand(repo, options) {
|
|
|
687
702
|
dryRunCount++;
|
|
688
703
|
}
|
|
689
704
|
}
|
|
690
|
-
const skillsContent2 = await fetchSkillsJson(repo);
|
|
705
|
+
const skillsContent2 = await fetchSkillsJson(repo, dir);
|
|
691
706
|
if (skillsContent2 !== null) {
|
|
692
707
|
try {
|
|
693
708
|
const skillsManifest = parseSkills(
|
|
@@ -746,7 +761,7 @@ async function addCommand(repo, options) {
|
|
|
746
761
|
}
|
|
747
762
|
}
|
|
748
763
|
let skillsInstalled = 0;
|
|
749
|
-
const skillsContent = await fetchSkillsJson(repo);
|
|
764
|
+
const skillsContent = await fetchSkillsJson(repo, dir);
|
|
750
765
|
if (skillsContent === null) {
|
|
751
766
|
info("No skills.json found \u2014 skipping skills installation.");
|
|
752
767
|
} else {
|
|
@@ -794,7 +809,7 @@ async function addCommand(repo, options) {
|
|
|
794
809
|
const summary = skillsInstalled > 0 ? `${installed} file(s) + ${skillsInstalled} skill(s)` : `${installed} file(s)`;
|
|
795
810
|
success(`Done! ${summary} installed, ${skipped} skipped.`);
|
|
796
811
|
if (installed + skillsInstalled > 0) {
|
|
797
|
-
await recordDownload(repo).catch(() => {
|
|
812
|
+
await recordDownload(repo, dir).catch(() => {
|
|
798
813
|
});
|
|
799
814
|
}
|
|
800
815
|
} catch (err) {
|
|
@@ -807,6 +822,7 @@ async function addCommand(repo, options) {
|
|
|
807
822
|
|
|
808
823
|
// src/commands/init.ts
|
|
809
824
|
var import_node_child_process2 = require("child_process");
|
|
825
|
+
var import_promises2 = require("fs/promises");
|
|
810
826
|
var import_node_path4 = require("path");
|
|
811
827
|
var import_prompts4 = __toESM(require("prompts"), 1);
|
|
812
828
|
var DEFAULT_VERSION = "0.1.0";
|
|
@@ -912,10 +928,16 @@ function buildFileList(name, description, author, targets) {
|
|
|
912
928
|
}
|
|
913
929
|
return files;
|
|
914
930
|
}
|
|
915
|
-
async function initCommand() {
|
|
931
|
+
async function initCommand(options) {
|
|
916
932
|
try {
|
|
917
933
|
const cwd = process.cwd();
|
|
918
|
-
const
|
|
934
|
+
const dir = options.dir;
|
|
935
|
+
const baseDir = dir ? (0, import_node_path4.join)(cwd, dir) : cwd;
|
|
936
|
+
if (dir) {
|
|
937
|
+
await (0, import_promises2.mkdir)(baseDir, { recursive: true });
|
|
938
|
+
}
|
|
939
|
+
const repoName = (0, import_node_path4.basename)(cwd);
|
|
940
|
+
const defaultName = dir ? `${repoName}/${dir}` : repoName;
|
|
919
941
|
const gitUser = getGitHubUsername();
|
|
920
942
|
info("Scaffolding a new updose boilerplate...\n");
|
|
921
943
|
let cancelled = false;
|
|
@@ -972,7 +994,7 @@ async function initCommand() {
|
|
|
972
994
|
let created = 0;
|
|
973
995
|
let skipped = 0;
|
|
974
996
|
for (const file of files) {
|
|
975
|
-
const destPath = (0, import_node_path4.join)(
|
|
997
|
+
const destPath = (0, import_node_path4.join)(baseDir, file.path);
|
|
976
998
|
const exists = await fileExists(destPath);
|
|
977
999
|
if (exists) {
|
|
978
1000
|
const { action } = await (0, import_prompts4.default)({
|
|
@@ -998,14 +1020,21 @@ async function initCommand() {
|
|
|
998
1020
|
success(`Boilerplate scaffolded! (${created} created, ${skipped} skipped)`);
|
|
999
1021
|
console.log();
|
|
1000
1022
|
info("Next steps:");
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
);
|
|
1023
|
+
const editDirs = targets.map((t) => dir ? `${dir}/${t}/` : `${t}/`);
|
|
1024
|
+
console.log(` 1. Edit your boilerplate files in ${editDirs.join(", ")}`);
|
|
1004
1025
|
console.log(" 2. Push to GitHub");
|
|
1005
|
-
console.log(" 3. Publish with: npx updose publish");
|
|
1006
1026
|
console.log(
|
|
1007
|
-
`
|
|
1027
|
+
` 3. Publish with: npx updose publish${dir ? ` --dir ${dir}` : ""}`
|
|
1008
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
|
+
}
|
|
1009
1038
|
} catch (err) {
|
|
1010
1039
|
error(
|
|
1011
1040
|
err instanceof Error ? err.message : "An unexpected error occurred during init."
|
|
@@ -1015,7 +1044,7 @@ async function initCommand() {
|
|
|
1015
1044
|
}
|
|
1016
1045
|
|
|
1017
1046
|
// src/auth/github-oauth.ts
|
|
1018
|
-
var
|
|
1047
|
+
var import_promises3 = require("fs/promises");
|
|
1019
1048
|
var import_node_os = require("os");
|
|
1020
1049
|
var import_node_path5 = require("path");
|
|
1021
1050
|
var import_chalk2 = __toESM(require("chalk"), 1);
|
|
@@ -1032,7 +1061,7 @@ var AUTH_FILE_MODE = 384;
|
|
|
1032
1061
|
var MAX_POLL_INTERVAL_SEC = 60;
|
|
1033
1062
|
async function getStoredToken() {
|
|
1034
1063
|
try {
|
|
1035
|
-
const content = await (0,
|
|
1064
|
+
const content = await (0, import_promises3.readFile)(AUTH_FILE, "utf-8");
|
|
1036
1065
|
const data = JSON.parse(content);
|
|
1037
1066
|
return data.github_token ?? null;
|
|
1038
1067
|
} catch {
|
|
@@ -1041,7 +1070,7 @@ async function getStoredToken() {
|
|
|
1041
1070
|
}
|
|
1042
1071
|
async function getStoredAuth() {
|
|
1043
1072
|
try {
|
|
1044
|
-
const content = await (0,
|
|
1073
|
+
const content = await (0, import_promises3.readFile)(AUTH_FILE, "utf-8");
|
|
1045
1074
|
return JSON.parse(content);
|
|
1046
1075
|
} catch {
|
|
1047
1076
|
return null;
|
|
@@ -1097,8 +1126,8 @@ async function login() {
|
|
|
1097
1126
|
throw new Error("GitHub authorization failed");
|
|
1098
1127
|
}
|
|
1099
1128
|
const username = await fetchUsername(token);
|
|
1100
|
-
await (0,
|
|
1101
|
-
await (0,
|
|
1129
|
+
await (0, import_promises3.mkdir)(AUTH_DIR, { recursive: true });
|
|
1130
|
+
await (0, import_promises3.writeFile)(
|
|
1102
1131
|
AUTH_FILE,
|
|
1103
1132
|
JSON.stringify(
|
|
1104
1133
|
{ github_token: token, github_username: username },
|
|
@@ -1183,7 +1212,7 @@ function sleep(ms) {
|
|
|
1183
1212
|
}
|
|
1184
1213
|
async function logout() {
|
|
1185
1214
|
try {
|
|
1186
|
-
await (0,
|
|
1215
|
+
await (0, import_promises3.unlink)(AUTH_FILE);
|
|
1187
1216
|
return true;
|
|
1188
1217
|
} catch {
|
|
1189
1218
|
return false;
|
|
@@ -1218,20 +1247,32 @@ async function logoutCommand() {
|
|
|
1218
1247
|
|
|
1219
1248
|
// src/commands/publish.ts
|
|
1220
1249
|
var import_node_child_process3 = require("child_process");
|
|
1221
|
-
var
|
|
1250
|
+
var import_node_fs = require("fs");
|
|
1251
|
+
var import_promises4 = require("fs/promises");
|
|
1222
1252
|
var import_node_path6 = require("path");
|
|
1223
1253
|
var import_chalk3 = __toESM(require("chalk"), 1);
|
|
1224
1254
|
var FETCH_TIMEOUT_MS3 = 1e4;
|
|
1225
|
-
async function publishCommand() {
|
|
1255
|
+
async function publishCommand(options) {
|
|
1226
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
|
+
}
|
|
1227
1264
|
let raw;
|
|
1228
1265
|
try {
|
|
1229
|
-
const content = await (0,
|
|
1266
|
+
const content = await (0, import_promises4.readFile)(
|
|
1267
|
+
(0, import_node_path6.join)(manifestDir, MANIFEST_FILENAME),
|
|
1268
|
+
"utf-8"
|
|
1269
|
+
);
|
|
1230
1270
|
raw = JSON.parse(content);
|
|
1231
1271
|
} catch (err) {
|
|
1232
1272
|
if (err.code === "ENOENT") {
|
|
1273
|
+
const location = dir ? `"${dir}"` : "current directory";
|
|
1233
1274
|
error(
|
|
1234
|
-
`No ${MANIFEST_FILENAME} found in
|
|
1275
|
+
`No ${MANIFEST_FILENAME} found in ${location}. Run \`updose init\` first.`
|
|
1235
1276
|
);
|
|
1236
1277
|
} else {
|
|
1237
1278
|
error(
|
|
@@ -1265,9 +1306,10 @@ async function publishCommand() {
|
|
|
1265
1306
|
process.exitCode = 1;
|
|
1266
1307
|
return;
|
|
1267
1308
|
}
|
|
1268
|
-
|
|
1309
|
+
const expectedName = dir ? `${repoName}/${dir}` : repoName;
|
|
1310
|
+
if (manifest.name.toLowerCase() !== expectedName.toLowerCase()) {
|
|
1269
1311
|
error(
|
|
1270
|
-
`Manifest name "${manifest.name}" does not match
|
|
1312
|
+
`Manifest name "${manifest.name}" does not match expected name "${expectedName}".`
|
|
1271
1313
|
);
|
|
1272
1314
|
process.exitCode = 1;
|
|
1273
1315
|
return;
|
|
@@ -1317,6 +1359,9 @@ Make sure you have pushed your code to GitHub.`
|
|
|
1317
1359
|
console.log(` Name: ${manifest.name}`);
|
|
1318
1360
|
console.log(` Version: ${manifest.version}`);
|
|
1319
1361
|
console.log(` Repository: ${repo}`);
|
|
1362
|
+
if (dir) {
|
|
1363
|
+
console.log(` Directory: ${dir}`);
|
|
1364
|
+
}
|
|
1320
1365
|
console.log(` Targets: ${manifest.targets.join(", ")}`);
|
|
1321
1366
|
if (manifest.tags?.length) {
|
|
1322
1367
|
console.log(` Tags: ${manifest.tags.join(", ")}`);
|
|
@@ -1338,11 +1383,15 @@ Make sure you have pushed your code to GitHub.`
|
|
|
1338
1383
|
targets: manifest.targets,
|
|
1339
1384
|
tags: manifest.tags
|
|
1340
1385
|
},
|
|
1341
|
-
token
|
|
1386
|
+
token,
|
|
1387
|
+
dir
|
|
1342
1388
|
);
|
|
1343
1389
|
spinner.success("Published successfully!");
|
|
1344
1390
|
console.log();
|
|
1345
|
-
|
|
1391
|
+
const installPath = dir ? `${repo}/${dir}` : repo;
|
|
1392
|
+
info(
|
|
1393
|
+
`Users can now install with: ${import_chalk3.default.cyan(`npx updose add ${installPath}`)}`
|
|
1394
|
+
);
|
|
1346
1395
|
} catch (err) {
|
|
1347
1396
|
spinner.fail("Publication failed");
|
|
1348
1397
|
error(err.message);
|
|
@@ -1421,17 +1470,18 @@ function formatResult(bp) {
|
|
|
1421
1470
|
if (bp.tags.length > 0) {
|
|
1422
1471
|
console.log(` ${bp.tags.map((t) => import_chalk4.default.dim(`#${t}`)).join(" ")}`);
|
|
1423
1472
|
}
|
|
1424
|
-
|
|
1473
|
+
const repoPath = bp.dir ? `${bp.repo}/${bp.dir}` : bp.repo;
|
|
1474
|
+
console.log(` ${import_chalk4.default.dim(repoPath)}`);
|
|
1425
1475
|
console.log();
|
|
1426
1476
|
}
|
|
1427
1477
|
|
|
1428
1478
|
// src/index.ts
|
|
1429
1479
|
var program = new import_commander.Command();
|
|
1430
|
-
program.name("updose").description("AI coding tool boilerplate marketplace").version("0.
|
|
1480
|
+
program.name("updose").description("AI coding tool boilerplate marketplace").version("0.3.0");
|
|
1431
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);
|
|
1432
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);
|
|
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);
|
|
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);
|
|
1435
1485
|
program.command("login").description("Log in to GitHub").action(loginCommand);
|
|
1436
1486
|
program.command("logout").description("Log out from GitHub").action(logoutCommand);
|
|
1437
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) {
|
|
@@ -286,6 +286,18 @@ function optionalStringArray(obj, key) {
|
|
|
286
286
|
// src/core/github.ts
|
|
287
287
|
var GITHUB_RAW = "https://raw.githubusercontent.com";
|
|
288
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
|
+
}
|
|
289
301
|
function parseRepo(repo) {
|
|
290
302
|
const parts = repo.split("/");
|
|
291
303
|
if (parts.length !== 2 || !parts[0] || !parts[1]) {
|
|
@@ -365,11 +377,12 @@ async function fetchFile(repo, path) {
|
|
|
365
377
|
}
|
|
366
378
|
return res.text();
|
|
367
379
|
}
|
|
368
|
-
async function fetchManifest(repo) {
|
|
369
|
-
const
|
|
380
|
+
async function fetchManifest(repo, dir) {
|
|
381
|
+
const path = dir ? `${dir}/${MANIFEST_FILENAME}` : MANIFEST_FILENAME;
|
|
382
|
+
const content = await fetchFile(repo, path);
|
|
370
383
|
if (content === null) {
|
|
371
384
|
throw new Error(
|
|
372
|
-
`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?`
|
|
373
386
|
);
|
|
374
387
|
}
|
|
375
388
|
let raw;
|
|
@@ -407,8 +420,9 @@ async function fetchRepoTree(repo) {
|
|
|
407
420
|
}
|
|
408
421
|
return data.tree.filter((entry) => entry.type === "blob");
|
|
409
422
|
}
|
|
410
|
-
async function fetchSkillsJson(repo) {
|
|
411
|
-
|
|
423
|
+
async function fetchSkillsJson(repo, dir) {
|
|
424
|
+
const path = dir ? `${dir}/${SKILLS_FILENAME}` : SKILLS_FILENAME;
|
|
425
|
+
return fetchFile(repo, path);
|
|
412
426
|
}
|
|
413
427
|
|
|
414
428
|
// src/core/installer.ts
|
|
@@ -591,15 +605,16 @@ async function settledPool(tasks, limit) {
|
|
|
591
605
|
);
|
|
592
606
|
return results;
|
|
593
607
|
}
|
|
594
|
-
async function addCommand(
|
|
608
|
+
async function addCommand(repoInput, options) {
|
|
595
609
|
try {
|
|
610
|
+
const { repo, dir } = parseRepoInput(repoInput);
|
|
596
611
|
const cwd = process.cwd();
|
|
597
612
|
const skipPrompts = options.yes ?? false;
|
|
598
613
|
const dryRun = options.dryRun ?? false;
|
|
599
614
|
const manifestSpinner = createSpinner("Fetching updose.json...").start();
|
|
600
615
|
let manifest;
|
|
601
616
|
try {
|
|
602
|
-
manifest = await fetchManifest(repo);
|
|
617
|
+
manifest = await fetchManifest(repo, dir);
|
|
603
618
|
manifestSpinner.success(
|
|
604
619
|
`Found ${manifest.name} by ${manifest.author} (v${manifest.version})`
|
|
605
620
|
);
|
|
@@ -632,7 +647,7 @@ async function addCommand(repo, options) {
|
|
|
632
647
|
const filesByTarget = /* @__PURE__ */ new Map();
|
|
633
648
|
for (const target of selectedTargets) {
|
|
634
649
|
const sourceDir = getSourceDir(target);
|
|
635
|
-
const prefix = `${sourceDir}/`;
|
|
650
|
+
const prefix = dir ? `${dir}/${sourceDir}/` : `${sourceDir}/`;
|
|
636
651
|
const files = tree.filter((entry) => {
|
|
637
652
|
if (!entry.path.startsWith(prefix)) return false;
|
|
638
653
|
const relativePath = entry.path.slice(prefix.length);
|
|
@@ -665,7 +680,7 @@ async function addCommand(repo, options) {
|
|
|
665
680
|
dryRunCount++;
|
|
666
681
|
}
|
|
667
682
|
}
|
|
668
|
-
const skillsContent2 = await fetchSkillsJson(repo);
|
|
683
|
+
const skillsContent2 = await fetchSkillsJson(repo, dir);
|
|
669
684
|
if (skillsContent2 !== null) {
|
|
670
685
|
try {
|
|
671
686
|
const skillsManifest = parseSkills(
|
|
@@ -724,7 +739,7 @@ async function addCommand(repo, options) {
|
|
|
724
739
|
}
|
|
725
740
|
}
|
|
726
741
|
let skillsInstalled = 0;
|
|
727
|
-
const skillsContent = await fetchSkillsJson(repo);
|
|
742
|
+
const skillsContent = await fetchSkillsJson(repo, dir);
|
|
728
743
|
if (skillsContent === null) {
|
|
729
744
|
info("No skills.json found \u2014 skipping skills installation.");
|
|
730
745
|
} else {
|
|
@@ -772,7 +787,7 @@ async function addCommand(repo, options) {
|
|
|
772
787
|
const summary = skillsInstalled > 0 ? `${installed} file(s) + ${skillsInstalled} skill(s)` : `${installed} file(s)`;
|
|
773
788
|
success(`Done! ${summary} installed, ${skipped} skipped.`);
|
|
774
789
|
if (installed + skillsInstalled > 0) {
|
|
775
|
-
await recordDownload(repo).catch(() => {
|
|
790
|
+
await recordDownload(repo, dir).catch(() => {
|
|
776
791
|
});
|
|
777
792
|
}
|
|
778
793
|
} catch (err) {
|
|
@@ -785,6 +800,7 @@ async function addCommand(repo, options) {
|
|
|
785
800
|
|
|
786
801
|
// src/commands/init.ts
|
|
787
802
|
import { execSync } from "child_process";
|
|
803
|
+
import { mkdir as mkdir2 } from "fs/promises";
|
|
788
804
|
import { basename, join as join2 } from "path";
|
|
789
805
|
import prompts2 from "prompts";
|
|
790
806
|
var DEFAULT_VERSION = "0.1.0";
|
|
@@ -890,10 +906,16 @@ function buildFileList(name, description, author, targets) {
|
|
|
890
906
|
}
|
|
891
907
|
return files;
|
|
892
908
|
}
|
|
893
|
-
async function initCommand() {
|
|
909
|
+
async function initCommand(options) {
|
|
894
910
|
try {
|
|
895
911
|
const cwd = process.cwd();
|
|
896
|
-
const
|
|
912
|
+
const dir = options.dir;
|
|
913
|
+
const baseDir = dir ? join2(cwd, dir) : cwd;
|
|
914
|
+
if (dir) {
|
|
915
|
+
await mkdir2(baseDir, { recursive: true });
|
|
916
|
+
}
|
|
917
|
+
const repoName = basename(cwd);
|
|
918
|
+
const defaultName = dir ? `${repoName}/${dir}` : repoName;
|
|
897
919
|
const gitUser = getGitHubUsername();
|
|
898
920
|
info("Scaffolding a new updose boilerplate...\n");
|
|
899
921
|
let cancelled = false;
|
|
@@ -950,7 +972,7 @@ async function initCommand() {
|
|
|
950
972
|
let created = 0;
|
|
951
973
|
let skipped = 0;
|
|
952
974
|
for (const file of files) {
|
|
953
|
-
const destPath = join2(
|
|
975
|
+
const destPath = join2(baseDir, file.path);
|
|
954
976
|
const exists = await fileExists(destPath);
|
|
955
977
|
if (exists) {
|
|
956
978
|
const { action } = await prompts2({
|
|
@@ -976,14 +998,21 @@ async function initCommand() {
|
|
|
976
998
|
success(`Boilerplate scaffolded! (${created} created, ${skipped} skipped)`);
|
|
977
999
|
console.log();
|
|
978
1000
|
info("Next steps:");
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
);
|
|
1001
|
+
const editDirs = targets.map((t) => dir ? `${dir}/${t}/` : `${t}/`);
|
|
1002
|
+
console.log(` 1. Edit your boilerplate files in ${editDirs.join(", ")}`);
|
|
982
1003
|
console.log(" 2. Push to GitHub");
|
|
983
|
-
console.log(" 3. Publish with: npx updose publish");
|
|
984
1004
|
console.log(
|
|
985
|
-
`
|
|
1005
|
+
` 3. Publish with: npx updose publish${dir ? ` --dir ${dir}` : ""}`
|
|
986
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
|
+
}
|
|
987
1016
|
} catch (err) {
|
|
988
1017
|
error(
|
|
989
1018
|
err instanceof Error ? err.message : "An unexpected error occurred during init."
|
|
@@ -993,7 +1022,7 @@ async function initCommand() {
|
|
|
993
1022
|
}
|
|
994
1023
|
|
|
995
1024
|
// src/auth/github-oauth.ts
|
|
996
|
-
import { mkdir as
|
|
1025
|
+
import { mkdir as mkdir3, readFile as readFile2, unlink, writeFile as writeFile2 } from "fs/promises";
|
|
997
1026
|
import { homedir } from "os";
|
|
998
1027
|
import { join as join3 } from "path";
|
|
999
1028
|
import chalk2 from "chalk";
|
|
@@ -1075,7 +1104,7 @@ async function login() {
|
|
|
1075
1104
|
throw new Error("GitHub authorization failed");
|
|
1076
1105
|
}
|
|
1077
1106
|
const username = await fetchUsername(token);
|
|
1078
|
-
await
|
|
1107
|
+
await mkdir3(AUTH_DIR, { recursive: true });
|
|
1079
1108
|
await writeFile2(
|
|
1080
1109
|
AUTH_FILE,
|
|
1081
1110
|
JSON.stringify(
|
|
@@ -1196,20 +1225,32 @@ async function logoutCommand() {
|
|
|
1196
1225
|
|
|
1197
1226
|
// src/commands/publish.ts
|
|
1198
1227
|
import { execSync as execSync2 } from "child_process";
|
|
1228
|
+
import { existsSync } from "fs";
|
|
1199
1229
|
import { readFile as readFile3 } from "fs/promises";
|
|
1200
1230
|
import { join as join4 } from "path";
|
|
1201
1231
|
import chalk3 from "chalk";
|
|
1202
1232
|
var FETCH_TIMEOUT_MS3 = 1e4;
|
|
1203
|
-
async function publishCommand() {
|
|
1233
|
+
async function publishCommand(options) {
|
|
1204
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
|
+
}
|
|
1205
1242
|
let raw;
|
|
1206
1243
|
try {
|
|
1207
|
-
const content = await readFile3(
|
|
1244
|
+
const content = await readFile3(
|
|
1245
|
+
join4(manifestDir, MANIFEST_FILENAME),
|
|
1246
|
+
"utf-8"
|
|
1247
|
+
);
|
|
1208
1248
|
raw = JSON.parse(content);
|
|
1209
1249
|
} catch (err) {
|
|
1210
1250
|
if (err.code === "ENOENT") {
|
|
1251
|
+
const location = dir ? `"${dir}"` : "current directory";
|
|
1211
1252
|
error(
|
|
1212
|
-
`No ${MANIFEST_FILENAME} found in
|
|
1253
|
+
`No ${MANIFEST_FILENAME} found in ${location}. Run \`updose init\` first.`
|
|
1213
1254
|
);
|
|
1214
1255
|
} else {
|
|
1215
1256
|
error(
|
|
@@ -1243,9 +1284,10 @@ async function publishCommand() {
|
|
|
1243
1284
|
process.exitCode = 1;
|
|
1244
1285
|
return;
|
|
1245
1286
|
}
|
|
1246
|
-
|
|
1287
|
+
const expectedName = dir ? `${repoName}/${dir}` : repoName;
|
|
1288
|
+
if (manifest.name.toLowerCase() !== expectedName.toLowerCase()) {
|
|
1247
1289
|
error(
|
|
1248
|
-
`Manifest name "${manifest.name}" does not match
|
|
1290
|
+
`Manifest name "${manifest.name}" does not match expected name "${expectedName}".`
|
|
1249
1291
|
);
|
|
1250
1292
|
process.exitCode = 1;
|
|
1251
1293
|
return;
|
|
@@ -1295,6 +1337,9 @@ Make sure you have pushed your code to GitHub.`
|
|
|
1295
1337
|
console.log(` Name: ${manifest.name}`);
|
|
1296
1338
|
console.log(` Version: ${manifest.version}`);
|
|
1297
1339
|
console.log(` Repository: ${repo}`);
|
|
1340
|
+
if (dir) {
|
|
1341
|
+
console.log(` Directory: ${dir}`);
|
|
1342
|
+
}
|
|
1298
1343
|
console.log(` Targets: ${manifest.targets.join(", ")}`);
|
|
1299
1344
|
if (manifest.tags?.length) {
|
|
1300
1345
|
console.log(` Tags: ${manifest.tags.join(", ")}`);
|
|
@@ -1316,11 +1361,15 @@ Make sure you have pushed your code to GitHub.`
|
|
|
1316
1361
|
targets: manifest.targets,
|
|
1317
1362
|
tags: manifest.tags
|
|
1318
1363
|
},
|
|
1319
|
-
token
|
|
1364
|
+
token,
|
|
1365
|
+
dir
|
|
1320
1366
|
);
|
|
1321
1367
|
spinner.success("Published successfully!");
|
|
1322
1368
|
console.log();
|
|
1323
|
-
|
|
1369
|
+
const installPath = dir ? `${repo}/${dir}` : repo;
|
|
1370
|
+
info(
|
|
1371
|
+
`Users can now install with: ${chalk3.cyan(`npx updose add ${installPath}`)}`
|
|
1372
|
+
);
|
|
1324
1373
|
} catch (err) {
|
|
1325
1374
|
spinner.fail("Publication failed");
|
|
1326
1375
|
error(err.message);
|
|
@@ -1399,17 +1448,18 @@ function formatResult(bp) {
|
|
|
1399
1448
|
if (bp.tags.length > 0) {
|
|
1400
1449
|
console.log(` ${bp.tags.map((t) => chalk4.dim(`#${t}`)).join(" ")}`);
|
|
1401
1450
|
}
|
|
1402
|
-
|
|
1451
|
+
const repoPath = bp.dir ? `${bp.repo}/${bp.dir}` : bp.repo;
|
|
1452
|
+
console.log(` ${chalk4.dim(repoPath)}`);
|
|
1403
1453
|
console.log();
|
|
1404
1454
|
}
|
|
1405
1455
|
|
|
1406
1456
|
// src/index.ts
|
|
1407
1457
|
var program = new Command();
|
|
1408
|
-
program.name("updose").description("AI coding tool boilerplate marketplace").version("0.
|
|
1458
|
+
program.name("updose").description("AI coding tool boilerplate marketplace").version("0.3.0");
|
|
1409
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);
|
|
1410
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);
|
|
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);
|
|
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);
|
|
1413
1463
|
program.command("login").description("Log in to GitHub").action(loginCommand);
|
|
1414
1464
|
program.command("logout").description("Log out from GitHub").action(logoutCommand);
|
|
1415
1465
|
program.parse();
|