Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 25 additions & 4 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,25 +2,46 @@ name: Publish to npm
on:
push:
tags:
- 'v*'
- "v*"

permissions:
contents: read

jobs:
publish:
runs-on: ubuntu-latest
permissions:
# Needed to create the GitHub release.
contents: write
# Mints the OIDC token npm exchanges for publish rights. Trusted
# publishing attaches a provenance statement on its own, so there is
# no --provenance flag and no npm token to rotate.
id-token: write
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 22
# Trusted publishing needs npm 11.5.1+, which Node 24 ships.
node-version: 24
registry-url: https://registry.npmjs.org
- run: corepack enable
- run: yarn install --immutable
- run: yarn typecheck
- run: yarn build

- name: Check the tag matches the package version
run: |
tag="${GITHUB_REF_NAME#v}"
version="$(node -p 'require("./package.json").version')"
if [ "$tag" != "$version" ]; then
echo "Tag $GITHUB_REF_NAME does not match package.json version $version" >&2
exit 1
fi

- name: Publish to npm
run: npm publish --provenance --access public
run: npm publish

- name: Create GitHub Release
run: gh release create ${{ github.ref_name }} --generate-notes
run: gh release create "$GITHUB_REF_NAME" --generate-notes
env:
GH_TOKEN: ${{ github.token }}
942 changes: 0 additions & 942 deletions .yarn/releases/yarn-4.12.0.cjs

This file was deleted.

1,000 changes: 1,000 additions & 0 deletions .yarn/releases/yarn-4.18.0.cjs

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion .yarnrc.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,4 @@ enableGlobalCache: false

nodeLinker: node-modules

yarnPath: .yarn/releases/yarn-4.12.0.cjs
yarnPath: .yarn/releases/yarn-4.18.0.cjs
63 changes: 55 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,30 +1,65 @@
# create-stratal

Scaffold a new [Stratal](https://github.com/strataljs/stratal) project from official templates.
Scaffold a new [Stratal](https://stratal.dev) project from official templates.

[Documentation](https://stratal.dev) · [Stratal on GitHub](https://github.com/strataljs/stratal) · [Examples](https://github.com/strataljs/examples)

## Usage

```bash
# npm
npm create stratal my-app
npm create stratal@latest my-app

# yarn
yarn create stratal my-app
yarn create stratal@latest my-app

# pnpm
pnpm create stratal my-app
pnpm create stratal@latest my-app
```

This launches an interactive prompt to pick a template, then downloads it into `my-app/`.

Every new project also gets the [Stratal agent skills](https://skills.sh/strataljs/stratal), so coding agents
know the framework from the first prompt. Pass `--no-skills` to leave them out.

## CLI Flags

| Flag | Description |
| --- | --- |
| `-t, --template <name>` | Skip the template picker and use a specific template |
| `-p, --package-manager <name>` | Install with `npm`, `yarn`, `pnpm`, or `bun` |
| `--no-install` | Skip installing dependencies |
| `--no-skills` | Skip the Stratal agent skills |
| `--force` | Replace the contents of the target directory |
| `-y, --yes` | Accept defaults instead of asking |
| `-l, --list` | List all available templates |
| `-h, --help` | Show help |

## Non-interactive use

Every question has a flag, so the CLI can run unattended — useful in CI or
when an agent drives it. Outside a terminal it never prompts: anything left
unanswered is reported as an error naming the flag to pass, so a run either
completes or exits non-zero, and never hangs waiting for input.

```bash
# Fully specified
npm create stratal@latest my-app -t crud-api -p npm

# Scaffold only, no install and no skills
npm create stratal@latest my-app -t crud-api --no-install --no-skills

# Accept every default (hello-world, the package manager that invoked it)
npm create stratal@latest my-app --yes
```

`--force` empties the target directory before writing the template, so
nothing from a previous project is left behind. `--yes` alone will not do
this. The template is downloaded to a staging directory first, so a failed
download leaves the existing directory untouched.

Run `--list` to see the template names `--template` accepts.

## Available Templates

| Template | Description |
Expand All @@ -45,23 +80,35 @@ This launches an interactive prompt to pick a template, then downloads it into `
| `factories` | Test data factories with Faker.js and state modifiers |
| `multi-connection-database` | Multi-connection database with per-connection schema management |
| `workers` | Durable Objects, Workflows, and WorkerEntrypoints with DI |
| `commands` | Custom Quarry CLI commands |
| `inertia` | Inertia.js v3 with React SSR, typed props, and flash messages |

## Examples

```bash
# Interactive mode
npm create stratal my-app
npm create stratal@latest my-app

# Use a specific template
npm create stratal my-app --template crud-api
npm create stratal@latest my-app --template crud-api

# Short flag
npm create stratal my-app -t openapi
npm create stratal@latest my-app -t openapi

# List templates
npm create stratal -- --list
npm create stratal@latest -- --list

# Skip the agent skills
npm create stratal@latest my-app -t crud-api --no-skills
```

## Links

- [Documentation](https://stratal.dev)
- [Stratal on GitHub](https://github.com/strataljs/stratal)
- [Examples](https://github.com/strataljs/examples)
- [Report an issue](https://github.com/strataljs/create-stratal/issues)

## License

MIT
21 changes: 12 additions & 9 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,7 @@
"node": ">=22.0.0"
},
"type": "module",
"bin": {
"create-stratal": "dist/index.mjs"
},
"bin": "dist/index.mjs",
"files": [
"dist"
],
Expand All @@ -35,17 +33,22 @@
],
"license": "MIT",
"dependencies": {
"@clack/prompts": "^1.0.1",
"giget": "^3.1.2",
"nypm": "^0.6.5",
"@clack/prompts": "^1.8.1",
"giget": "^3.3.1",
"nypm": "^0.6.10",
"picocolors": "^1.1.1"
},
"devDependencies": {
"@types/node": "^25.3.2",
"@types/node": "^26.6.2",
"tsup": "^8.5.1",
"typescript": "^5.9.3"
"typescript": "^7.0.2"
},
"dependenciesMeta": {
"esbuild": {
"built": false
}
},
"packageManager": "yarn@4.12.0",
"packageManager": "yarn@4.18.0",
"publishConfig": {
"access": "public",
"registry": "https://registry.npmjs.org"
Expand Down
125 changes: 105 additions & 20 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,33 +1,73 @@
import * as p from "@clack/prompts";
import pc from "picocolors";
import { templates } from "./templates.js";
import { runPrompts, runPackageManagerPrompt } from "./prompts.js";
import {
runPrompts,
resolvePackageManager,
isPackageManager,
packageManagers,
type CliOptions,
} from "./prompts.js";
import { installDependencies } from "nypm";
import { scaffold } from "./scaffold.js";
import { scaffold, installSkills } from "./scaffold.js";

const HELP = `
${pc.bold("create-stratal")} — Scaffold a new Stratal project

${pc.bold("Usage:")}
npm create stratal [project-name] [options]
yarn create stratal [project-name] [options]
pnpm create stratal [project-name] [options]
npm create stratal@latest [project-name] [options]
yarn create stratal@latest [project-name] [options]
pnpm create stratal@latest [project-name] [options]

${pc.bold("Options:")}
-t, --template <name> Use a specific template (skip interactive selection)
-l, --list List available templates
-h, --help Show this help message
-t, --template <name> Template to use
-p, --package-manager <name> Install with npm, yarn, pnpm, or bun
--no-install Skip installing dependencies
--no-skills Skip the Stratal agent skills
--force Replace the contents of the target directory
-y, --yes Accept defaults instead of asking
-l, --list List available templates
-h, --help Show this help message

${pc.bold("Examples:")}
npm create stratal my-app
npm create stratal my-app --template hello-world
npm create stratal my-app -t crud-api
npm create stratal@latest my-app
npm create stratal@latest my-app --template hello-world
npm create stratal@latest my-app -t crud-api

${pc.bold("Non-interactive:")}
Every question above has a flag, so the CLI never waits for input when
each one is answered. Outside a terminal it will not ask at all: pass
--yes for defaults, or name a template and a package manager.

npm create stratal@latest my-app -t crud-api -p npm
npm create stratal@latest my-app -t crud-api --no-install --no-skills
npm create stratal@latest my-app --yes
`.trim();

function parseArgs(argv: string[]) {
const args: { name?: string; template?: string; help: boolean; list: boolean } = {
interface ParsedArgs extends CliOptions {
help: boolean;
list: boolean;
}

class ArgError extends Error {}

/** Reads the value that follows a flag, e.g. "--template crud-api". */
function takeValue(argv: string[], index: number, flag: string): string {
const value = argv[index];
if (value === undefined || value.startsWith("-")) {
throw new ArgError(`${flag} needs a value.`);
}
return value;
}

function parseArgs(argv: string[]): ParsedArgs {
const args: ParsedArgs = {
help: false,
list: false,
install: true,
skills: true,
force: false,
yes: false,
};

let i = 0;
Expand All @@ -37,10 +77,30 @@ function parseArgs(argv: string[]) {
args.help = true;
} else if (arg === "--list" || arg === "-l") {
args.list = true;
} else if (arg === "--yes" || arg === "-y") {
args.yes = true;
} else if (arg === "--force") {
args.force = true;
} else if (arg === "--no-install") {
args.install = false;
} else if (arg === "--no-skills") {
args.skills = false;
} else if (arg === "--template" || arg === "-t") {
args.template = argv[++i];
} else if (!arg.startsWith("-")) {
args.template = takeValue(argv, ++i, arg);
} else if (arg === "--package-manager" || arg === "-p") {
const value = takeValue(argv, ++i, arg);
if (!isPackageManager(value)) {
throw new ArgError(
`Unknown package manager "${value}". Use ${packageManagers.join(", ")}.`,
);
}
args.packageManager = value;
} else if (arg.startsWith("-")) {
throw new ArgError(`Unknown option "${arg}". Run with --help.`);
} else if (args.name === undefined) {
args.name = arg;
} else {
throw new ArgError(`Unexpected argument "${arg}". Run with --help.`);
}
i++;
}
Expand All @@ -49,7 +109,15 @@ function parseArgs(argv: string[]) {
}

async function main() {
const args = parseArgs(process.argv.slice(2));
let args: ParsedArgs;
try {
args = parseArgs(process.argv.slice(2));
} catch (error) {
console.error(
pc.red(error instanceof ArgError ? error.message : String(error)),
);
process.exit(1);
}

if (args.help) {
console.log(HELP);
Expand All @@ -60,15 +128,15 @@ async function main() {
console.log(pc.bold("\nAvailable templates:\n"));
for (const t of templates) {
const shortName = t.dir.replace(/^\d+-/, "");
console.log(` ${pc.cyan(shortName.padEnd(20))} ${pc.dim(t.description)}`);
console.log(` ${pc.cyan(shortName.padEnd(26))} ${pc.dim(t.description)}`);
}
console.log();
return;
}

p.intro(pc.bgCyan(pc.black(" create-stratal ")));

const result = await runPrompts(args.name, args.template);
const result = await runPrompts(args);
if (!result) {
process.exit(1);
}
Expand All @@ -77,7 +145,12 @@ async function main() {
s.start(`Scaffolding ${pc.cyan(result.projectName)}...`);

try {
await scaffold(result.template, result.targetDir, result.projectName);
await scaffold(
result.template,
result.targetDir,
result.projectName,
result.replace,
);
s.stop(`Scaffolded ${pc.cyan(result.projectName)}`);
} catch (error) {
s.stop("Failed to scaffold project");
Expand All @@ -87,7 +160,19 @@ async function main() {
process.exit(1);
}

const packageManager = await runPackageManagerPrompt();
if (args.skills) {
const skillsSpinner = p.spinner();
skillsSpinner.start("Adding Stratal agent skills...");
try {
await installSkills(result.targetDir);
skillsSpinner.stop("Added Stratal agent skills");
} catch {
// The project is usable without them, so this never fails the scaffold.
skillsSpinner.stop("Couldn't add the agent skills. Skipping.");
}
}

const packageManager = await resolvePackageManager(args);

if (packageManager) {
const installSpinner = p.spinner();
Expand Down
Loading
Loading