@oaklandzoo/ostup 0.1.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/LICENSE +21 -0
- package/README.md +191 -0
- package/bin/cli.mjs +150 -0
- package/package.json +58 -0
- package/src/config.mjs +41 -0
- package/src/credential-prompts.mjs +117 -0
- package/src/env-loader.mjs +27 -0
- package/src/exec.mjs +78 -0
- package/src/mvp-flow.mjs +219 -0
- package/src/preflight.mjs +55 -0
- package/src/project-prompts.mjs +220 -0
- package/src/prompts.mjs +60 -0
- package/src/scaffold.mjs +112 -0
- package/src/steps/github.mjs +21 -0
- package/src/steps/ingest.mjs +121 -0
- package/src/steps/inject.mjs +22 -0
- package/src/steps/next-app.mjs +41 -0
- package/src/steps/protection.mjs +181 -0
- package/src/steps/vercel.mjs +46 -0
- package/src/substitute.mjs +44 -0
- package/src/summary.mjs +34 -0
- package/src/templates.mjs +41 -0
- package/src/update.mjs +26 -0
- package/templates/.claude/commands/bootstrap.md +111 -0
- package/templates/.claude/commands/create-prd.md +85 -0
- package/templates/.claude/commands/generate-tasks.md +74 -0
- package/templates/.claude/commands/prompt-end.md +129 -0
- package/templates/.claude/commands/prompt-mid.md +74 -0
- package/templates/.claude/commands/prompt-start.md +85 -0
- package/templates/.ostup-config.yml.example +10 -0
- package/templates/AGENTS.md +33 -0
- package/templates/CLAUDE.md +256 -0
- package/templates/HANDOFF.md +49 -0
- package/templates/README.md +15 -0
- package/templates/_gitignore +9 -0
- package/templates/docs/ARCHITECTURE.md +59 -0
- package/templates/docs/MANUAL_TASKS.md +20 -0
- package/templates/docs/PROJECT_STATE.md +41 -0
- package/templates/docs/SESSION_NOTES.md +29 -0
- package/templates/inputs/README.md +25 -0
- package/templates/inputs/images/.gitkeep +0 -0
- package/templates/inputs/notes/.gitkeep +0 -0
- package/templates/inputs/references/.gitkeep +0 -0
- package/templates/inputs/research/.gitkeep +0 -0
- package/templates/tasks/.gitkeep +0 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 GG
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
# ostup
|
|
2
|
+
|
|
3
|
+
Scaffold a new client project with one command. Get a live GitHub repo
|
|
4
|
+
and live Vercel deploy URL in under five minutes.
|
|
5
|
+
|
|
6
|
+
## What this does
|
|
7
|
+
|
|
8
|
+
When you run this tool, it will:
|
|
9
|
+
|
|
10
|
+
1. Ask you a few questions about the project
|
|
11
|
+
2. Create a folder on your machine with a Next.js starter site
|
|
12
|
+
3. Create a private GitHub repository for the project
|
|
13
|
+
4. Deploy the site to Vercel and give you a live URL
|
|
14
|
+
5. Set up institutional memory files (CLAUDE.md, AGENTS.md) so any CLI
|
|
15
|
+
agent (Claude Code, Codex, Gemini CLI) knows your project rules
|
|
16
|
+
6. Create an `inputs/` folder inside the project where you can drop any
|
|
17
|
+
prior materials (research, reference repos, screenshots, brand
|
|
18
|
+
assets) you want the agent to have on hand
|
|
19
|
+
|
|
20
|
+
## What you need before running it
|
|
21
|
+
|
|
22
|
+
### One-time machine setup (you only do this once)
|
|
23
|
+
|
|
24
|
+
1. Install Node.js 20 or higher. Download from https://nodejs.org
|
|
25
|
+
|
|
26
|
+
2. Install GitHub CLI. In Terminal:
|
|
27
|
+
```
|
|
28
|
+
brew install gh
|
|
29
|
+
```
|
|
30
|
+
Then log in:
|
|
31
|
+
```
|
|
32
|
+
gh auth login
|
|
33
|
+
```
|
|
34
|
+
Choose: GitHub.com, then HTTPS, then "Login with a web browser."
|
|
35
|
+
Follow the prompts.
|
|
36
|
+
|
|
37
|
+
3. Install Vercel CLI. In Terminal:
|
|
38
|
+
```
|
|
39
|
+
npm install -g vercel
|
|
40
|
+
```
|
|
41
|
+
Then log in:
|
|
42
|
+
```
|
|
43
|
+
vercel login
|
|
44
|
+
```
|
|
45
|
+
Choose your email login method.
|
|
46
|
+
|
|
47
|
+
### Accounts you need
|
|
48
|
+
|
|
49
|
+
- GitHub account: free at https://github.com/signup
|
|
50
|
+
- Vercel account: free at https://vercel.com/signup (sign in with GitHub)
|
|
51
|
+
|
|
52
|
+
If you do not have these accounts, the tool will pause and walk you
|
|
53
|
+
through creating them when you reach that step.
|
|
54
|
+
|
|
55
|
+
## Install
|
|
56
|
+
|
|
57
|
+
Two paths. Pick the one that matches how you got ostup.
|
|
58
|
+
|
|
59
|
+
### Path A: from npm (after `ostup` is published)
|
|
60
|
+
|
|
61
|
+
```
|
|
62
|
+
npx ostup init
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
That is the whole install. `npx` downloads ostup on first run.
|
|
66
|
+
|
|
67
|
+
As of right now, `ostup` is not yet published to npm. Use Path B below.
|
|
68
|
+
|
|
69
|
+
### Path B: from source (today's path)
|
|
70
|
+
|
|
71
|
+
If you cloned or downloaded https://github.com/DubsFan/goodshin:
|
|
72
|
+
|
|
73
|
+
```
|
|
74
|
+
cd /path/to/goodshin/ostup
|
|
75
|
+
npm install # downloads ostup's own dependencies
|
|
76
|
+
npm link # adds `ostup` to your PATH globally
|
|
77
|
+
ostup --version # should print 0.1.0
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
After `npm link` you can run `ostup` from any folder. You only do this
|
|
81
|
+
once per machine.
|
|
82
|
+
|
|
83
|
+
## Use
|
|
84
|
+
|
|
85
|
+
In Terminal, navigate to the parent folder where you want the new
|
|
86
|
+
project to be created:
|
|
87
|
+
|
|
88
|
+
```
|
|
89
|
+
cd ~/Projects
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
Then run:
|
|
93
|
+
|
|
94
|
+
```
|
|
95
|
+
ostup init
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
The tool will ask you a series of questions. Answer each one. When the
|
|
99
|
+
tool finishes, you will see a live deploy URL. Open it in your browser
|
|
100
|
+
to confirm the site is up.
|
|
101
|
+
|
|
102
|
+
## Questions the tool will ask
|
|
103
|
+
|
|
104
|
+
| Question | What to answer |
|
|
105
|
+
|---|---|
|
|
106
|
+
| Project name | A short kebab-case name like `client-name` or `widget-shop` |
|
|
107
|
+
| Display name | The human readable version, like "Client Name" |
|
|
108
|
+
| Description | One sentence, 10 to 200 characters |
|
|
109
|
+
| Profile | Press Enter to accept `goodshin` |
|
|
110
|
+
| Stack | Press Enter to accept `next` (Next.js) |
|
|
111
|
+
| Visibility | Press Enter to accept `private` |
|
|
112
|
+
| GitHub owner | Press Enter to accept your GitHub username |
|
|
113
|
+
| Vercel scope | Press Enter to accept your Vercel scope |
|
|
114
|
+
| Prior materials? | Answer y if you have a folder of research, reference repos, images, etc. to bring in. Tool will ask for a path and copy them into `inputs/`. Answer n to skip; you can drop files into `inputs/` later. |
|
|
115
|
+
|
|
116
|
+
If the tool detects you are not logged in to GitHub or Vercel, it will
|
|
117
|
+
print step-by-step instructions and pause until you finish.
|
|
118
|
+
|
|
119
|
+
## What you get when it finishes
|
|
120
|
+
|
|
121
|
+
- A local folder at `./<project-name>` with the Next.js site code
|
|
122
|
+
- A private repo at `https://github.com/<your-username>/<project-name>`
|
|
123
|
+
- A live deploy at a `*.vercel.app` URL
|
|
124
|
+
- CLAUDE.md, AGENTS.md, README.md inside the project, filled in with
|
|
125
|
+
your project details
|
|
126
|
+
- An `inputs/` folder for any operator-supplied materials, with a
|
|
127
|
+
README explaining what goes where
|
|
128
|
+
|
|
129
|
+
## What to do next
|
|
130
|
+
|
|
131
|
+
Open the new folder in your editor and start working. To use a CLI
|
|
132
|
+
agent:
|
|
133
|
+
```
|
|
134
|
+
cd <project-name>
|
|
135
|
+
claude
|
|
136
|
+
```
|
|
137
|
+
Or replace `claude` with `codex`, `gemini`, or your preferred agent.
|
|
138
|
+
The CLAUDE.md and AGENTS.md files tell the agent your conventions so
|
|
139
|
+
you do not have to repeat them every session.
|
|
140
|
+
|
|
141
|
+
## Troubleshooting
|
|
142
|
+
|
|
143
|
+
| Problem | Fix |
|
|
144
|
+
|---|---|
|
|
145
|
+
| "node: command not found" | Install Node 20+ from https://nodejs.org |
|
|
146
|
+
| "gh: command not found" | Run `brew install gh` |
|
|
147
|
+
| "vercel: command not found" | Run `npm install -g vercel` |
|
|
148
|
+
| "gh auth required" | Run `gh auth login` |
|
|
149
|
+
| "vercel auth required" | Run `vercel login` |
|
|
150
|
+
| "Project name invalid" | Use only lowercase letters, numbers, and hyphens |
|
|
151
|
+
| "Repo already exists" | Pick a different project name |
|
|
152
|
+
| Vercel deploy hangs more than 5 minutes | Cancel with Ctrl-C, run again |
|
|
153
|
+
| Deploy URL returns 401 | The tool tried to auto-disable Vercel deployment protection but could not. Open Vercel dashboard, find your project, Settings, Deployment Protection, Disable. |
|
|
154
|
+
| "ingest path not found" | The path you gave does not exist. Re-run and provide a valid absolute or relative path. |
|
|
155
|
+
|
|
156
|
+
## Advanced: API tokens instead of interactive login
|
|
157
|
+
|
|
158
|
+
If you want to use API tokens (for CI, automation, or to avoid the
|
|
159
|
+
browser login flow), copy `.env.example` to `.env` and fill in your
|
|
160
|
+
tokens. The tool reads credentials in this order:
|
|
161
|
+
|
|
162
|
+
1. Environment variables (`GH_TOKEN`, `VERCEL_TOKEN`)
|
|
163
|
+
2. `.env` file in the current directory
|
|
164
|
+
3. Interactive CLI auth from `gh` and `vercel`
|
|
165
|
+
|
|
166
|
+
Any one of those is enough. Never commit your `.env` to git.
|
|
167
|
+
|
|
168
|
+
For full automation including automatic disable of Vercel deployment
|
|
169
|
+
protection on team accounts, set `VERCEL_TOKEN` in your environment or
|
|
170
|
+
`.env`. Without it, the tool falls back to reading the Vercel CLI auth
|
|
171
|
+
file on macOS at `~/Library/Application Support/com.vercel.cli/auth.json`.
|
|
172
|
+
On a fresh machine where you have not run `vercel login` yet, that file
|
|
173
|
+
will not exist, so `VERCEL_TOKEN` is the reliable path. Get a token at
|
|
174
|
+
https://vercel.com/account/tokens.
|
|
175
|
+
|
|
176
|
+
## Advanced: bring materials in non-interactively
|
|
177
|
+
|
|
178
|
+
Pass `--ingest <path>` to copy a folder of operator-supplied materials
|
|
179
|
+
into the new project's `inputs/` folder without being prompted:
|
|
180
|
+
|
|
181
|
+
```
|
|
182
|
+
npx ostup init --yes --name my-app --ingest ~/Desktop/client-handoff
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
The tool recursively copies the contents and writes
|
|
186
|
+
`inputs/INGEST_MANIFEST.md` listing what was brought in.
|
|
187
|
+
|
|
188
|
+
## Support
|
|
189
|
+
|
|
190
|
+
This tool is maintained at https://github.com/DubsFan/goodshin
|
|
191
|
+
Open an issue there if you hit a bug or need help.
|
package/bin/cli.mjs
ADDED
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// cli.mjs: parse argv, load .env, dispatch to a subcommand (init | update).
|
|
3
|
+
import { readFile } from 'node:fs/promises';
|
|
4
|
+
import { dirname, resolve } from 'node:path';
|
|
5
|
+
import { fileURLToPath } from 'node:url';
|
|
6
|
+
import { loadDotEnv } from '../src/env-loader.mjs';
|
|
7
|
+
import { setDryRun } from '../src/exec.mjs';
|
|
8
|
+
|
|
9
|
+
loadDotEnv();
|
|
10
|
+
|
|
11
|
+
const PKG_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..');
|
|
12
|
+
|
|
13
|
+
const SUBCOMMANDS = new Set(['init', 'update']);
|
|
14
|
+
|
|
15
|
+
async function readPkg() {
|
|
16
|
+
const raw = await readFile(resolve(PKG_ROOT, 'package.json'), 'utf8');
|
|
17
|
+
return JSON.parse(raw);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function parseArgs(argv) {
|
|
21
|
+
const flags = {};
|
|
22
|
+
const positional = [];
|
|
23
|
+
for (let i = 0; i < argv.length; i++) {
|
|
24
|
+
const a = argv[i];
|
|
25
|
+
if (a === '--yes' || a === '-y') flags.yes = true;
|
|
26
|
+
else if (a === '--force') flags.force = true;
|
|
27
|
+
else if (a === '--dry-run') flags.dryRun = true;
|
|
28
|
+
else if (a === '--update') flags.update = true;
|
|
29
|
+
else if (a === '--kit-only') flags.kitOnly = true;
|
|
30
|
+
else if (a === '--version' || a === '-v') flags.version = true;
|
|
31
|
+
else if (a === '--help' || a === '-h') flags.help = true;
|
|
32
|
+
else if (a === '--profile') flags.profile = argv[++i];
|
|
33
|
+
else if (a.startsWith('--profile=')) flags.profile = a.slice('--profile='.length);
|
|
34
|
+
else if (a === '--name') flags.name = argv[++i];
|
|
35
|
+
else if (a.startsWith('--name=')) flags.name = a.slice('--name='.length);
|
|
36
|
+
else if (a === '--config') flags.config = argv[++i];
|
|
37
|
+
else if (a.startsWith('--config=')) flags.config = a.slice('--config='.length);
|
|
38
|
+
else if (a === '--ingest') flags.ingest = argv[++i];
|
|
39
|
+
else if (a.startsWith('--ingest=')) flags.ingest = a.slice('--ingest='.length);
|
|
40
|
+
else if (a.startsWith('-')) {
|
|
41
|
+
process.stderr.write(`unknown flag: ${a}\n`);
|
|
42
|
+
process.exit(1);
|
|
43
|
+
} else {
|
|
44
|
+
positional.push(a);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return { flags, positional };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function printHelp() {
|
|
51
|
+
process.stdout.write(
|
|
52
|
+
[
|
|
53
|
+
'ostup: scaffold a new repo with the Ostup Agent Kit plus GitHub and Vercel.',
|
|
54
|
+
'',
|
|
55
|
+
'Usage:',
|
|
56
|
+
' ostup <command> [flags]',
|
|
57
|
+
'',
|
|
58
|
+
'Commands:',
|
|
59
|
+
' init Scaffold a new project (interactive or with --yes).',
|
|
60
|
+
' update Refresh bundled templates from the pinned source.',
|
|
61
|
+
'',
|
|
62
|
+
'Flags for `ostup init`:',
|
|
63
|
+
' --yes, -y Accept defaults. Still prompts where no default exists.',
|
|
64
|
+
' --force Allow scaffolding into a non-empty target dir.',
|
|
65
|
+
' --dry-run Print every action without running any subprocess.',
|
|
66
|
+
' --profile <name> Skip the profile prompt (goodshin or default).',
|
|
67
|
+
' --name <kebab> Skip the projectName prompt.',
|
|
68
|
+
' --ingest <path> Copy operator materials from <path> into inputs/.',
|
|
69
|
+
' --kit-only Drop the markdown kit into a target dir, no GitHub or Vercel.',
|
|
70
|
+
' --config <path> Read .ostup-config.yml from this path (kit-only mode).',
|
|
71
|
+
'',
|
|
72
|
+
'Global flags:',
|
|
73
|
+
' --version, -v Print version and exit.',
|
|
74
|
+
' --help, -h Print this help and exit.',
|
|
75
|
+
'',
|
|
76
|
+
].join('\n')
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const { flags, positional } = parseArgs(process.argv.slice(2));
|
|
81
|
+
|
|
82
|
+
if (flags.dryRun) setDryRun(true);
|
|
83
|
+
|
|
84
|
+
if (flags.version) {
|
|
85
|
+
const pkg = await readPkg();
|
|
86
|
+
process.stdout.write(`${pkg.version}\n`);
|
|
87
|
+
process.exit(0);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (flags.help) {
|
|
91
|
+
printHelp();
|
|
92
|
+
process.exit(0);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const subcommand = flags.update ? 'update' : (positional[0] || null);
|
|
96
|
+
|
|
97
|
+
if (!subcommand) {
|
|
98
|
+
printHelp();
|
|
99
|
+
process.exit(0);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (!SUBCOMMANDS.has(subcommand)) {
|
|
103
|
+
process.stderr.write(`unknown subcommand: ${subcommand}\nRun "ostup --help" for usage.\n`);
|
|
104
|
+
process.exit(1);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const subPositional = flags.update ? positional : positional.slice(1);
|
|
108
|
+
|
|
109
|
+
if (subcommand === 'update') {
|
|
110
|
+
const { update } = await import('../src/update.mjs');
|
|
111
|
+
try {
|
|
112
|
+
await update();
|
|
113
|
+
process.exit(0);
|
|
114
|
+
} catch (err) {
|
|
115
|
+
process.stderr.write(`${err.message}\n`);
|
|
116
|
+
const userErrors = new Set(['CONFIG_NOT_FOUND', 'UPDATE_NOT_CONFIGURED']);
|
|
117
|
+
process.exit(userErrors.has(err.code) ? 1 : 2);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// subcommand === 'init'
|
|
122
|
+
if (flags.kitOnly) {
|
|
123
|
+
const { scaffold } = await import('../src/scaffold.mjs');
|
|
124
|
+
const targetDir = subPositional[0] || process.cwd();
|
|
125
|
+
try {
|
|
126
|
+
await scaffold({ targetDir, flags });
|
|
127
|
+
process.exit(0);
|
|
128
|
+
} catch (err) {
|
|
129
|
+
process.stderr.write(`${err.message}\n`);
|
|
130
|
+
const userErrors = new Set(['TARGET_NOT_EMPTY', 'CONFIG_NOT_FOUND', 'NO_TTY']);
|
|
131
|
+
process.exit(userErrors.has(err.code) ? 1 : 2);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const { runMvp } = await import('../src/mvp-flow.mjs');
|
|
136
|
+
try {
|
|
137
|
+
await runMvp({ flags });
|
|
138
|
+
process.exit(0);
|
|
139
|
+
} catch (err) {
|
|
140
|
+
process.stderr.write(`${err.message}\n`);
|
|
141
|
+
const userErrors = new Set([
|
|
142
|
+
'NO_TTY',
|
|
143
|
+
'USER_ABORT',
|
|
144
|
+
'TARGET_NOT_EMPTY',
|
|
145
|
+
'NO_GH_CREDS',
|
|
146
|
+
'NO_VERCEL_CREDS',
|
|
147
|
+
'INGEST_PATH_NOT_FOUND',
|
|
148
|
+
]);
|
|
149
|
+
process.exit(userErrors.has(err.code) ? 1 : 2);
|
|
150
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@oaklandzoo/ostup",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Scaffolds a new repo with the Ostup Agent Kit pre-installed: slash commands, doc templates, and a clean working state.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"ostup": "bin/cli.mjs"
|
|
8
|
+
},
|
|
9
|
+
"publishConfig": {
|
|
10
|
+
"access": "public"
|
|
11
|
+
},
|
|
12
|
+
"scripts": {
|
|
13
|
+
"test": "node --test 'test/**/*.test.mjs'",
|
|
14
|
+
"prepublishOnly": "npm test"
|
|
15
|
+
},
|
|
16
|
+
"engines": {
|
|
17
|
+
"node": ">=18"
|
|
18
|
+
},
|
|
19
|
+
"license": "MIT",
|
|
20
|
+
"author": "GG",
|
|
21
|
+
"repository": {
|
|
22
|
+
"type": "git",
|
|
23
|
+
"url": "git+https://github.com/DubsFan/goodshin.git",
|
|
24
|
+
"directory": "ostup"
|
|
25
|
+
},
|
|
26
|
+
"homepage": "https://github.com/DubsFan/goodshin#readme",
|
|
27
|
+
"bugs": {
|
|
28
|
+
"url": "https://github.com/DubsFan/goodshin/issues"
|
|
29
|
+
},
|
|
30
|
+
"keywords": [
|
|
31
|
+
"claude",
|
|
32
|
+
"claude-code",
|
|
33
|
+
"codex",
|
|
34
|
+
"gemini-cli",
|
|
35
|
+
"scaffold",
|
|
36
|
+
"agent",
|
|
37
|
+
"template",
|
|
38
|
+
"init",
|
|
39
|
+
"starter",
|
|
40
|
+
"prd",
|
|
41
|
+
"nextjs",
|
|
42
|
+
"vercel"
|
|
43
|
+
],
|
|
44
|
+
"files": [
|
|
45
|
+
"bin/",
|
|
46
|
+
"src/",
|
|
47
|
+
"templates/",
|
|
48
|
+
"LICENSE",
|
|
49
|
+
"README.md"
|
|
50
|
+
],
|
|
51
|
+
"dependencies": {
|
|
52
|
+
"@clack/prompts": "^1.4.0",
|
|
53
|
+
"execa": "^9.6.1",
|
|
54
|
+
"kleur": "^4.1.5",
|
|
55
|
+
"prompts": "^2.4.2",
|
|
56
|
+
"yaml": "^2.9.0"
|
|
57
|
+
}
|
|
58
|
+
}
|
package/src/config.mjs
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises';
|
|
2
|
+
import { existsSync } from 'node:fs';
|
|
3
|
+
import { resolve } from 'node:path';
|
|
4
|
+
import { parse as parseYaml } from 'yaml';
|
|
5
|
+
|
|
6
|
+
export async function loadConfig({ targetDir, configPath }) {
|
|
7
|
+
const path = configPath
|
|
8
|
+
? resolve(configPath)
|
|
9
|
+
: resolve(targetDir, '.ostup-config.yml');
|
|
10
|
+
|
|
11
|
+
if (!existsSync(path)) {
|
|
12
|
+
if (configPath) {
|
|
13
|
+
const err = new Error(`config file not found: ${path}`);
|
|
14
|
+
err.code = 'CONFIG_NOT_FOUND';
|
|
15
|
+
throw err;
|
|
16
|
+
}
|
|
17
|
+
return {};
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const raw = await readFile(path, 'utf8');
|
|
21
|
+
const parsed = parseYaml(raw);
|
|
22
|
+
if (parsed == null || typeof parsed !== 'object') return {};
|
|
23
|
+
return parsed;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function mergeValues({ config, prompts, defaults }) {
|
|
27
|
+
return {
|
|
28
|
+
projectName: pick('projectName', config, prompts, defaults),
|
|
29
|
+
purpose: pick('purpose', config, prompts, defaults),
|
|
30
|
+
owner: pick('owner', config, prompts, defaults),
|
|
31
|
+
stack: pick('stack', config, prompts, defaults),
|
|
32
|
+
deploy: pick('deploy', config, prompts, defaults),
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function pick(key, ...sources) {
|
|
37
|
+
for (const src of sources) {
|
|
38
|
+
if (src && src[key] != null && src[key] !== '') return src[key];
|
|
39
|
+
}
|
|
40
|
+
return undefined;
|
|
41
|
+
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
// credential-prompts.mjs: detect GitHub and Vercel credentials, prompt and persist only if missing.
|
|
2
|
+
import * as p from '@clack/prompts';
|
|
3
|
+
import { execSync } from 'node:child_process';
|
|
4
|
+
|
|
5
|
+
export function checkGithubAuth({ env = process.env, runner = defaultCmdOk } = {}) {
|
|
6
|
+
if (env.GH_TOKEN) return { ok: true, source: 'env-or-dotenv' };
|
|
7
|
+
if (runner('gh auth status')) return { ok: true, source: 'gh-cli' };
|
|
8
|
+
return { ok: false };
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function checkVercelAuth({ env = process.env, runner = defaultCmdOk } = {}) {
|
|
12
|
+
if (env.VERCEL_TOKEN) return { ok: true, source: 'env-or-dotenv' };
|
|
13
|
+
if (runner('vercel whoami')) return { ok: true, source: 'vercel-cli' };
|
|
14
|
+
return { ok: false };
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function defaultCmdOk(cmd) {
|
|
18
|
+
try {
|
|
19
|
+
execSync(cmd, { stdio: ['ignore', 'ignore', 'ignore'] });
|
|
20
|
+
return true;
|
|
21
|
+
} catch {
|
|
22
|
+
return false;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const GH_BLURB = [
|
|
27
|
+
'',
|
|
28
|
+
'GitHub credentials not found.',
|
|
29
|
+
'',
|
|
30
|
+
'If you do not have a GitHub account yet:',
|
|
31
|
+
' 1. Open https://github.com/signup',
|
|
32
|
+
' 2. Create an account, then return here.',
|
|
33
|
+
'',
|
|
34
|
+
'To create a Personal Access Token:',
|
|
35
|
+
' 1. Open https://github.com/settings/personal-access-tokens',
|
|
36
|
+
' 2. Click Generate new token (fine-grained).',
|
|
37
|
+
' 3. Name: ostup-init',
|
|
38
|
+
' 4. Resource owner: your username',
|
|
39
|
+
' 5. Repository access: All repositories',
|
|
40
|
+
' 6. Permissions:',
|
|
41
|
+
' - Administration: Read and write',
|
|
42
|
+
' - Contents: Read and write',
|
|
43
|
+
' - Metadata: Read',
|
|
44
|
+
' 7. Generate token, copy the value.',
|
|
45
|
+
'',
|
|
46
|
+
].join('\n');
|
|
47
|
+
|
|
48
|
+
const VERCEL_BLURB = [
|
|
49
|
+
'',
|
|
50
|
+
'Vercel credentials not found.',
|
|
51
|
+
'',
|
|
52
|
+
'If you do not have a Vercel account yet:',
|
|
53
|
+
' 1. Open https://vercel.com/signup',
|
|
54
|
+
' 2. Create an account, then return here.',
|
|
55
|
+
'',
|
|
56
|
+
'To create a Personal Access Token:',
|
|
57
|
+
' 1. Open https://vercel.com/account/tokens',
|
|
58
|
+
' 2. Click Create Token.',
|
|
59
|
+
' 3. Name: ostup-init',
|
|
60
|
+
' 4. Scope: Full Account (default for personal tokens).',
|
|
61
|
+
' 5. Create, copy the value.',
|
|
62
|
+
'',
|
|
63
|
+
].join('\n');
|
|
64
|
+
|
|
65
|
+
export async function promptForGithubToken() {
|
|
66
|
+
process.stdout.write(GH_BLURB);
|
|
67
|
+
const token = await p.password({
|
|
68
|
+
message: 'Paste your GitHub Personal Access Token (leave blank to skip)',
|
|
69
|
+
});
|
|
70
|
+
if (p.isCancel(token)) return null;
|
|
71
|
+
return typeof token === 'string' && token.trim() ? token.trim() : null;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export async function promptForVercelToken() {
|
|
75
|
+
process.stdout.write(VERCEL_BLURB);
|
|
76
|
+
const token = await p.password({
|
|
77
|
+
message: 'Paste your Vercel Personal Access Token (leave blank to skip)',
|
|
78
|
+
});
|
|
79
|
+
if (p.isCancel(token)) return null;
|
|
80
|
+
return typeof token === 'string' && token.trim() ? token.trim() : null;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export async function ensureCredentials({ stack = 'next' } = {}) {
|
|
84
|
+
const collected = {};
|
|
85
|
+
|
|
86
|
+
const gh = checkGithubAuth();
|
|
87
|
+
if (gh.ok) {
|
|
88
|
+
process.stdout.write(`GitHub: using existing auth (${gh.source}).\n`);
|
|
89
|
+
} else {
|
|
90
|
+
const token = await promptForGithubToken();
|
|
91
|
+
if (!token) {
|
|
92
|
+
const err = new Error('GitHub credentials are required.');
|
|
93
|
+
err.code = 'NO_GH_CREDS';
|
|
94
|
+
throw err;
|
|
95
|
+
}
|
|
96
|
+
process.env.GH_TOKEN = token;
|
|
97
|
+
collected.GH_TOKEN = token;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (stack !== 'none') {
|
|
101
|
+
const v = checkVercelAuth();
|
|
102
|
+
if (v.ok) {
|
|
103
|
+
process.stdout.write(`Vercel: using existing auth (${v.source}).\n`);
|
|
104
|
+
} else {
|
|
105
|
+
const token = await promptForVercelToken();
|
|
106
|
+
if (!token) {
|
|
107
|
+
const err = new Error('Vercel credentials are required for stack=' + stack + '.');
|
|
108
|
+
err.code = 'NO_VERCEL_CREDS';
|
|
109
|
+
throw err;
|
|
110
|
+
}
|
|
111
|
+
process.env.VERCEL_TOKEN = token;
|
|
112
|
+
collected.VERCEL_TOKEN = token;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return { collected };
|
|
117
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
// env-loader.mjs: load .env from cwd into process.env without overwriting existing keys.
|
|
2
|
+
import { readFileSync, existsSync } from 'node:fs';
|
|
3
|
+
import { resolve } from 'node:path';
|
|
4
|
+
|
|
5
|
+
export function loadDotEnv(dir = process.cwd()) {
|
|
6
|
+
const path = resolve(dir, '.env');
|
|
7
|
+
const parsed = {};
|
|
8
|
+
if (!existsSync(path)) return parsed;
|
|
9
|
+
const raw = readFileSync(path, 'utf8');
|
|
10
|
+
for (const line of raw.split(/\r?\n/)) {
|
|
11
|
+
const trimmed = line.trim();
|
|
12
|
+
if (!trimmed || trimmed.startsWith('#')) continue;
|
|
13
|
+
const eq = trimmed.indexOf('=');
|
|
14
|
+
if (eq === -1) continue;
|
|
15
|
+
const key = trimmed.slice(0, eq).trim();
|
|
16
|
+
let value = trimmed.slice(eq + 1).trim();
|
|
17
|
+
if (
|
|
18
|
+
(value.startsWith('"') && value.endsWith('"')) ||
|
|
19
|
+
(value.startsWith("'") && value.endsWith("'"))
|
|
20
|
+
) {
|
|
21
|
+
value = value.slice(1, -1);
|
|
22
|
+
}
|
|
23
|
+
parsed[key] = value;
|
|
24
|
+
if (!(key in process.env)) process.env[key] = value;
|
|
25
|
+
}
|
|
26
|
+
return parsed;
|
|
27
|
+
}
|
package/src/exec.mjs
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
// exec.mjs: uniform shell call wrapper with logging, dry-run capture, and step-aware error formatting.
|
|
2
|
+
import { execa } from 'execa';
|
|
3
|
+
|
|
4
|
+
const HINTS = {
|
|
5
|
+
'gh repo create': 'Confirm the repo name is not already taken under this owner.',
|
|
6
|
+
'gh repo view': 'Verify the repo was created and you have access.',
|
|
7
|
+
'gh auth status': 'Run: gh auth login',
|
|
8
|
+
'vercel link': 'Confirm the Vercel scope exists and you have access.',
|
|
9
|
+
'vercel deploy': 'Run vercel logs <deployment-url> or rerun vercel manually to see the build error.',
|
|
10
|
+
'npx create-next-app': 'Check your internet connection and the create-next-app version compatibility.',
|
|
11
|
+
'git init': 'Confirm git is installed and you have write access to this directory.',
|
|
12
|
+
'git commit': 'Confirm git user.name and user.email are configured.',
|
|
13
|
+
'git push': 'Confirm you have push access to the remote.',
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
const defaultExeca = (cmd, args, opts) => execa(cmd, args, opts);
|
|
17
|
+
|
|
18
|
+
let _runner = defaultExeca;
|
|
19
|
+
let _dryRun = false;
|
|
20
|
+
let _captured = [];
|
|
21
|
+
let _quiet = false;
|
|
22
|
+
|
|
23
|
+
export function setRunner(fn) {
|
|
24
|
+
_runner = fn || defaultExeca;
|
|
25
|
+
}
|
|
26
|
+
export function resetRunner() {
|
|
27
|
+
_runner = defaultExeca;
|
|
28
|
+
}
|
|
29
|
+
export function setDryRun(v) {
|
|
30
|
+
_dryRun = Boolean(v);
|
|
31
|
+
_captured = [];
|
|
32
|
+
}
|
|
33
|
+
export function isDryRun() {
|
|
34
|
+
return _dryRun;
|
|
35
|
+
}
|
|
36
|
+
export function getCaptured() {
|
|
37
|
+
return _captured.slice();
|
|
38
|
+
}
|
|
39
|
+
export function setQuiet(v) {
|
|
40
|
+
_quiet = Boolean(v);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function hintFor(cmd, args) {
|
|
44
|
+
const sig = `${cmd} ${(args || []).slice(0, 2).join(' ')}`.trim();
|
|
45
|
+
for (const key of Object.keys(HINTS)) {
|
|
46
|
+
if (sig.startsWith(key)) return HINTS[key];
|
|
47
|
+
}
|
|
48
|
+
return 'See the command stderr above.';
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export async function run(step, cmd, args = [], opts = {}) {
|
|
52
|
+
const display = `${cmd} ${args.join(' ')}`.trim();
|
|
53
|
+
if (_dryRun) {
|
|
54
|
+
_captured.push({ step, cmd, args, opts });
|
|
55
|
+
if (!_quiet) process.stdout.write(`[${step}] would run: ${display}\n`);
|
|
56
|
+
return { stdout: '', stderr: '', exitCode: 0, dryRun: true };
|
|
57
|
+
}
|
|
58
|
+
if (!_quiet) process.stdout.write(`[${step}] start: ${display}\n`);
|
|
59
|
+
try {
|
|
60
|
+
return await _runner(cmd, args, { ...opts });
|
|
61
|
+
} catch (err) {
|
|
62
|
+
const stderr = (err && (err.stderr || err.shortMessage || err.message) || '').toString().trim();
|
|
63
|
+
process.stderr.write(
|
|
64
|
+
[
|
|
65
|
+
`[${step}] FAIL`,
|
|
66
|
+
` Command: ${display}`,
|
|
67
|
+
` Stderr: ${stderr || '(empty)'}`,
|
|
68
|
+
` Fix: ${hintFor(cmd, args)}`,
|
|
69
|
+
'Exit 1.',
|
|
70
|
+
].join('\n') + '\n'
|
|
71
|
+
);
|
|
72
|
+
const e = new Error(`step ${step} failed`);
|
|
73
|
+
e.code = 'STEP_FAILED';
|
|
74
|
+
e.step = step;
|
|
75
|
+
e.cause = err;
|
|
76
|
+
throw e;
|
|
77
|
+
}
|
|
78
|
+
}
|