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 +179 -23
- package/dist/index.cjs +111 -83
- package/dist/index.js +106 -78
- 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
|
|-------- |------------- |
|
|
@@ -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
|
-
|
|
133
|
+
Running `npx updose search` with no arguments returns popular boilerplates.
|
|
123
134
|
|
|
124
|
-
Results display the boilerplate name, version, author, description,
|
|
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 |
|
|
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
|
@@ -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({
|
|
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}/${
|
|
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
|
|
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
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
|
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)(
|
|
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
|
-
|
|
1002
|
-
|
|
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
|
-
`
|
|
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
|
|
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,
|
|
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,
|
|
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,
|
|
1101
|
-
await (0,
|
|
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,
|
|
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
|
|
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,
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
1393
|
-
const label = query ? `"${query}"` : "the given filters";
|
|
1394
|
-
|
|
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 ${
|
|
1424
|
+
info(`Found ${response.total} result(s) for ${label}:
|
|
1400
1425
|
`);
|
|
1401
|
-
for (const bp of
|
|
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(` ${
|
|
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
|
-
|
|
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.
|
|
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({
|
|
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}/${
|
|
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
|
|
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
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
|
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(
|
|
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
|
-
|
|
980
|
-
|
|
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
|
-
`
|
|
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
|
|
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
|
|
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(
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
1371
|
-
const label = query ? `"${query}"` : "the given filters";
|
|
1372
|
-
|
|
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 ${
|
|
1402
|
+
info(`Found ${response.total} result(s) for ${label}:
|
|
1378
1403
|
`);
|
|
1379
|
-
for (const bp of
|
|
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(` ${
|
|
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
|
-
|
|
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.
|
|
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();
|