@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.
Files changed (45) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +191 -0
  3. package/bin/cli.mjs +150 -0
  4. package/package.json +58 -0
  5. package/src/config.mjs +41 -0
  6. package/src/credential-prompts.mjs +117 -0
  7. package/src/env-loader.mjs +27 -0
  8. package/src/exec.mjs +78 -0
  9. package/src/mvp-flow.mjs +219 -0
  10. package/src/preflight.mjs +55 -0
  11. package/src/project-prompts.mjs +220 -0
  12. package/src/prompts.mjs +60 -0
  13. package/src/scaffold.mjs +112 -0
  14. package/src/steps/github.mjs +21 -0
  15. package/src/steps/ingest.mjs +121 -0
  16. package/src/steps/inject.mjs +22 -0
  17. package/src/steps/next-app.mjs +41 -0
  18. package/src/steps/protection.mjs +181 -0
  19. package/src/steps/vercel.mjs +46 -0
  20. package/src/substitute.mjs +44 -0
  21. package/src/summary.mjs +34 -0
  22. package/src/templates.mjs +41 -0
  23. package/src/update.mjs +26 -0
  24. package/templates/.claude/commands/bootstrap.md +111 -0
  25. package/templates/.claude/commands/create-prd.md +85 -0
  26. package/templates/.claude/commands/generate-tasks.md +74 -0
  27. package/templates/.claude/commands/prompt-end.md +129 -0
  28. package/templates/.claude/commands/prompt-mid.md +74 -0
  29. package/templates/.claude/commands/prompt-start.md +85 -0
  30. package/templates/.ostup-config.yml.example +10 -0
  31. package/templates/AGENTS.md +33 -0
  32. package/templates/CLAUDE.md +256 -0
  33. package/templates/HANDOFF.md +49 -0
  34. package/templates/README.md +15 -0
  35. package/templates/_gitignore +9 -0
  36. package/templates/docs/ARCHITECTURE.md +59 -0
  37. package/templates/docs/MANUAL_TASKS.md +20 -0
  38. package/templates/docs/PROJECT_STATE.md +41 -0
  39. package/templates/docs/SESSION_NOTES.md +29 -0
  40. package/templates/inputs/README.md +25 -0
  41. package/templates/inputs/images/.gitkeep +0 -0
  42. package/templates/inputs/notes/.gitkeep +0 -0
  43. package/templates/inputs/references/.gitkeep +0 -0
  44. package/templates/inputs/research/.gitkeep +0 -0
  45. 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
+ }