@ifi/oh-pi-skills 0.2.8 → 0.2.9
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/package.json +1 -1
- package/skills/flutter-serverpod-mvp/SKILL.md +199 -0
- package/skills/rust-workspace-bootstrap/SKILL.md +86 -0
- package/skills/rust-workspace-bootstrap/scaffold.js +222 -0
- package/skills/rust-workspace-bootstrap/template/.cargo/config.toml +7 -0
- package/skills/rust-workspace-bootstrap/template/.changeset/.gitkeep +0 -0
- package/skills/rust-workspace-bootstrap/template/.envrc +7 -0
- package/skills/rust-workspace-bootstrap/template/.github/actions/devenv/action.yml +42 -0
- package/skills/rust-workspace-bootstrap/template/.github/workflows/ci.yml +130 -0
- package/skills/rust-workspace-bootstrap/template/.github/workflows/coverage.yml +45 -0
- package/skills/rust-workspace-bootstrap/template/.github/workflows/docs-pages.yml +60 -0
- package/skills/rust-workspace-bootstrap/template/.github/workflows/release-preview.yml +52 -0
- package/skills/rust-workspace-bootstrap/template/.github/workflows/release.yml +87 -0
- package/skills/rust-workspace-bootstrap/template/.github/workflows/semver.yml +63 -0
- package/skills/rust-workspace-bootstrap/template/Cargo.toml +64 -0
- package/skills/rust-workspace-bootstrap/template/changelog.md +3 -0
- package/skills/rust-workspace-bootstrap/template/clippy.toml +1 -0
- package/skills/rust-workspace-bootstrap/template/crates/__CLI_CRATE__/Cargo.toml +20 -0
- package/skills/rust-workspace-bootstrap/template/crates/__CLI_CRATE__/src/main.rs +21 -0
- package/skills/rust-workspace-bootstrap/template/crates/__CORE_CRATE__/Cargo.toml +15 -0
- package/skills/rust-workspace-bootstrap/template/crates/__CORE_CRATE__/src/lib.rs +32 -0
- package/skills/rust-workspace-bootstrap/template/deny.toml +18 -0
- package/skills/rust-workspace-bootstrap/template/devenv.nix +214 -0
- package/skills/rust-workspace-bootstrap/template/devenv.yaml +10 -0
- package/skills/rust-workspace-bootstrap/template/docs/book.toml +6 -0
- package/skills/rust-workspace-bootstrap/template/docs/src/SUMMARY.md +3 -0
- package/skills/rust-workspace-bootstrap/template/docs/src/index.md +3 -0
- package/skills/rust-workspace-bootstrap/template/dprint.json +40 -0
- package/skills/rust-workspace-bootstrap/template/knope.toml +73 -0
- package/skills/rust-workspace-bootstrap/template/readme.md +77 -0
- package/skills/rust-workspace-bootstrap/template/rust-toolchain.toml +4 -0
- package/skills/rust-workspace-bootstrap/template/rustfmt.toml +21 -0
- package/skills/rust-workspace-bootstrap/template/scripts/release.sh +61 -0
package/package.json
CHANGED
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: flutter-serverpod-mvp
|
|
3
|
+
description:
|
|
4
|
+
Scaffold and evolve full-stack Flutter + Serverpod MVPs using devenv, Riverpod + Hooks,
|
|
5
|
+
strict i18n, and GoRouter shell routing patterns inspired by OpenBudget.
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# Flutter + Serverpod MVP (OpenBudget-style)
|
|
9
|
+
|
|
10
|
+
Use this skill when the user wants to:
|
|
11
|
+
|
|
12
|
+
- start a **new full-stack Flutter + Serverpod project**,
|
|
13
|
+
- add a major feature to an existing Flutter + Serverpod monorepo,
|
|
14
|
+
- enforce OpenBudget-style rules for **devenv**, **Riverpod + Hooks**, **i18n**, and **routing**.
|
|
15
|
+
|
|
16
|
+
## Core Standards (non-negotiable)
|
|
17
|
+
|
|
18
|
+
These conventions are extracted from the OpenBudget setup and should be applied by default.
|
|
19
|
+
|
|
20
|
+
1. **Workspace-first monorepo**
|
|
21
|
+
- Keep app/server/shared modules in one workspace.
|
|
22
|
+
- Use `melos` scripts from the workspace root.
|
|
23
|
+
|
|
24
|
+
2. **Hooks + Riverpod architecture**
|
|
25
|
+
- Use `HookConsumerWidget` for widgets that read providers.
|
|
26
|
+
- Use `HookWidget` for widgets without provider reads.
|
|
27
|
+
- Prefer custom hooks (`use*`) over ad-hoc widget state.
|
|
28
|
+
- Use `@riverpod`/`riverpod_annotation` providers, not manual legacy provider styles.
|
|
29
|
+
|
|
30
|
+
3. **Serverpod full-stack flow**
|
|
31
|
+
- Keep API/domain logic in `server/` with endpoint + service pattern.
|
|
32
|
+
- Generate protocol/client code whenever models/endpoints change.
|
|
33
|
+
- App calls Serverpod via a dedicated `serverpodClientProvider`.
|
|
34
|
+
|
|
35
|
+
4. **Strict i18n discipline**
|
|
36
|
+
- No hardcoded user-facing strings in UI.
|
|
37
|
+
- Use ARB + generated `AppLocalizations` API.
|
|
38
|
+
- Add and run a hardcoded text checker (OpenBudget-style `tools/check_localized_ui_text.dart`).
|
|
39
|
+
|
|
40
|
+
5. **Structured GoRouter routing**
|
|
41
|
+
- Route constants in `route_names.dart`.
|
|
42
|
+
- Router provider in `app_router.dart` via `@riverpod`.
|
|
43
|
+
- Auth redirect in a pure/testable helper function.
|
|
44
|
+
- Use `StatefulShellRoute.indexedStack` for bottom-tab apps with separate navigator stacks.
|
|
45
|
+
|
|
46
|
+
6. **devenv as the operational entrypoint**
|
|
47
|
+
- Local infra (Postgres/Redis) + scripts + process logs under devenv.
|
|
48
|
+
- `devenv up` should bring up backend dependencies and server.
|
|
49
|
+
|
|
50
|
+
## Recommended Project Layout
|
|
51
|
+
|
|
52
|
+
```text
|
|
53
|
+
<project>/
|
|
54
|
+
├── app/ # Flutter app
|
|
55
|
+
├── server/ # Serverpod backend
|
|
56
|
+
├── client/ # Generated serverpod protocol client
|
|
57
|
+
├── core/ # Shared Dart models/utilities (no Flutter)
|
|
58
|
+
├── ui/ # Shared Flutter UI package
|
|
59
|
+
├── lints/ # Centralized analyzer/lint config (optional but recommended)
|
|
60
|
+
├── test_utils/ # Shared test helpers
|
|
61
|
+
├── tools/ # Scripts (e.g. l10n hardcoded text check)
|
|
62
|
+
├── pubspec.yaml # Workspace root + melos config
|
|
63
|
+
├── devenv.nix # Development environment and scripts
|
|
64
|
+
├── devenv.yaml # devenv inputs
|
|
65
|
+
└── .github/workflows/ci.yml
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## Bootstrap Workflow for a New MVP
|
|
69
|
+
|
|
70
|
+
When asked to create a new project, execute this order:
|
|
71
|
+
|
|
72
|
+
1. **Collect project inputs**
|
|
73
|
+
- Project name, organization ID, app bundle IDs, default flavor, API hostnames.
|
|
74
|
+
|
|
75
|
+
2. **Scaffold workspace root**
|
|
76
|
+
- Root `pubspec.yaml` with Dart workspace members.
|
|
77
|
+
- `melos` scripts for analyze/test/generate/serverpod generation.
|
|
78
|
+
|
|
79
|
+
3. **Scaffold Serverpod package**
|
|
80
|
+
- `server/pubspec.yaml` with `serverpod` dependencies.
|
|
81
|
+
- `server/config/{development,test,production}.yaml`.
|
|
82
|
+
- `server/config/generator.yaml` pointing to `../client`.
|
|
83
|
+
- Initial endpoint/service files.
|
|
84
|
+
|
|
85
|
+
4. **Scaffold Flutter app package**
|
|
86
|
+
- Dependencies: `hooks_riverpod`, `flutter_hooks`, `go_router`,
|
|
87
|
+
`riverpod_annotation`, `serverpod_flutter`, auth package, localization deps.
|
|
88
|
+
- `l10n.yaml` configured with generated output directory.
|
|
89
|
+
- app bootstrap with `MaterialApp.router`, localization delegates, and router provider.
|
|
90
|
+
|
|
91
|
+
5. **Wire full-stack client access**
|
|
92
|
+
- Add `serverpodClientProvider` with environment override support.
|
|
93
|
+
- Ensure test runtime handling for connectivity monitor.
|
|
94
|
+
|
|
95
|
+
6. **Add i18n and routing foundations**
|
|
96
|
+
- `lib/l10n/app_en.arb` + generated localization output path.
|
|
97
|
+
- `route_names.dart` for all route/path constants.
|
|
98
|
+
- `app_router.dart` with auth redirects + shell routing where needed.
|
|
99
|
+
|
|
100
|
+
7. **Add devenv and CI**
|
|
101
|
+
- `devenv.nix` with scripts for lint/test/generate/server start.
|
|
102
|
+
- `devenv` GitHub composite action + CI jobs for lint/test/server/integration.
|
|
103
|
+
|
|
104
|
+
8. **Create first vertical slice**
|
|
105
|
+
- Auth + one core domain flow (e.g. projects/tasks/budgets).
|
|
106
|
+
- Endpoint → service → provider → screen.
|
|
107
|
+
- Unit/widget/integration tests for the slice.
|
|
108
|
+
|
|
109
|
+
## i18n Rules (OpenBudget pattern)
|
|
110
|
+
|
|
111
|
+
- Keep ARB files under `app/lib/l10n/`.
|
|
112
|
+
- Keep generated localization files under `app/lib/l10n/generated/`.
|
|
113
|
+
- Use `AppLocalizations.of(context)` for all visible text.
|
|
114
|
+
- Add a hardcoded-string checker script and run it in CI (`lint:l10n`).
|
|
115
|
+
- Only allow explicit opt-outs with inline comment markers (for rare cases).
|
|
116
|
+
|
|
117
|
+
Minimal `l10n.yaml` baseline:
|
|
118
|
+
|
|
119
|
+
```yaml
|
|
120
|
+
arb-dir: lib/l10n
|
|
121
|
+
template-arb-file: app_en.arb
|
|
122
|
+
output-localization-file: app_localizations.dart
|
|
123
|
+
output-dir: lib/l10n/generated
|
|
124
|
+
output-class: AppLocalizations
|
|
125
|
+
nullable-getter: false
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
## Routing Rules (OpenBudget pattern)
|
|
129
|
+
|
|
130
|
+
- Keep route names and paths in one constants file.
|
|
131
|
+
- Keep router creation inside a provider (`@riverpod GoRouter appRouter(Ref ref)`).
|
|
132
|
+
- Keep redirect logic in a separate helper for easy unit tests.
|
|
133
|
+
- For tabbed apps, use `StatefulShellRoute.indexedStack` with one navigator key per tab.
|
|
134
|
+
- Keep non-tab overlays outside shell branches.
|
|
135
|
+
|
|
136
|
+
## Provider + UI Rules
|
|
137
|
+
|
|
138
|
+
- Use feature-oriented modules.
|
|
139
|
+
- Follow flow: **endpoint/service (server)** → **client call** → **provider** → **UI widget**.
|
|
140
|
+
- Keep side effects in providers/services, not in build methods.
|
|
141
|
+
- Handle async UI with `AsyncValue.when` or pattern matching.
|
|
142
|
+
|
|
143
|
+
## devenv + Commands Contract
|
|
144
|
+
|
|
145
|
+
Include scripts equivalent to these responsibilities:
|
|
146
|
+
|
|
147
|
+
- `server:start`
|
|
148
|
+
- `runner:build`
|
|
149
|
+
- `runner:serverpod`
|
|
150
|
+
- `lint:analyze`
|
|
151
|
+
- `lint:l10n`
|
|
152
|
+
- `lint:all`
|
|
153
|
+
- `test:flutter`
|
|
154
|
+
- `test:all`
|
|
155
|
+
|
|
156
|
+
Target dev loop:
|
|
157
|
+
|
|
158
|
+
```bash
|
|
159
|
+
devenv shell
|
|
160
|
+
dart pub get
|
|
161
|
+
runner:build
|
|
162
|
+
runner:serverpod
|
|
163
|
+
devenv up
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
## CI Contract
|
|
167
|
+
|
|
168
|
+
At minimum, CI should run:
|
|
169
|
+
|
|
170
|
+
1. lint job (`lint:all`)
|
|
171
|
+
2. test job (Flutter + Dart/server tests)
|
|
172
|
+
3. server job with Postgres/Redis services
|
|
173
|
+
4. integration job (if integration tests exist)
|
|
174
|
+
|
|
175
|
+
Use a reusable setup action to install Nix/devenv, cache pub deps, and install Flutter via FVM.
|
|
176
|
+
|
|
177
|
+
## Feature Delivery Checklist (for agents)
|
|
178
|
+
|
|
179
|
+
Before marking work complete:
|
|
180
|
+
|
|
181
|
+
- [ ] Codegen run (Riverpod/build_runner + Serverpod)
|
|
182
|
+
- [ ] Generated files committed
|
|
183
|
+
- [ ] No hardcoded UI text
|
|
184
|
+
- [ ] New routes declared in `route_names.dart`
|
|
185
|
+
- [ ] Router/auth redirect tests updated
|
|
186
|
+
- [ ] Provider tests + widget tests added/updated
|
|
187
|
+
- [ ] Server endpoint/service tests added/updated
|
|
188
|
+
- [ ] `lint:all` and relevant tests passing
|
|
189
|
+
|
|
190
|
+
## What to produce when user asks for a "new MVP"
|
|
191
|
+
|
|
192
|
+
Always produce:
|
|
193
|
+
|
|
194
|
+
1. full folder skeleton,
|
|
195
|
+
2. root workspace config (`pubspec.yaml` + melos scripts),
|
|
196
|
+
3. app bootstrap + router + localization scaffolding,
|
|
197
|
+
4. serverpod config + initial endpoint/service,
|
|
198
|
+
5. devenv + CI baseline,
|
|
199
|
+
6. one implemented end-to-end feature slice with tests.
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: rust-workspace-bootstrap
|
|
3
|
+
description:
|
|
4
|
+
Scaffold a production-ready Rust workspace with knope changesets, devenv, and GitHub Actions CI/release workflows. Use when starting a new Rust project or monorepo.
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Rust Workspace Bootstrap
|
|
8
|
+
|
|
9
|
+
Generate a Rust workspace template inspired by the release/devenv/workflow structure used in
|
|
10
|
+
`mdt` and `pina`.
|
|
11
|
+
|
|
12
|
+
## What it scaffolds
|
|
13
|
+
|
|
14
|
+
- Cargo workspace with `core` + `cli` crates
|
|
15
|
+
- `knope.toml` with:
|
|
16
|
+
- `document-change`
|
|
17
|
+
- `release`
|
|
18
|
+
- `forced-release`
|
|
19
|
+
- `publish`
|
|
20
|
+
- `.changeset/` folder for change files
|
|
21
|
+
- `devenv.nix`, `devenv.yaml`, `.envrc`
|
|
22
|
+
- GitHub Actions:
|
|
23
|
+
- CI
|
|
24
|
+
- coverage
|
|
25
|
+
- semver checks
|
|
26
|
+
- release preview
|
|
27
|
+
- release assets
|
|
28
|
+
- docs-pages deployment
|
|
29
|
+
- Opinionated defaults for `rustfmt`, `clippy`, `deny`, `dprint`
|
|
30
|
+
|
|
31
|
+
## Usage
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
# Minimal
|
|
35
|
+
{baseDir}/scaffold.js --name acme-tool
|
|
36
|
+
|
|
37
|
+
# Recommended (in a separate worktree)
|
|
38
|
+
git worktree add ../acme-tool -b feat/bootstrap-acme-tool
|
|
39
|
+
cd ../acme-tool
|
|
40
|
+
/path/to/rust-workspace-bootstrap/scaffold.js \
|
|
41
|
+
--name acme-tool \
|
|
42
|
+
--owner your-github-org \
|
|
43
|
+
--repo acme-tool \
|
|
44
|
+
--description "CLI + core Rust workspace"
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## Options
|
|
48
|
+
|
|
49
|
+
- `--name <kebab-case>` (required)
|
|
50
|
+
- `--dir <path>` (optional, default: `./<name>`)
|
|
51
|
+
- `--owner <github-owner>` (optional, default: `your-github-org`)
|
|
52
|
+
- `--repo <github-repo>` (optional, default: `<name>`)
|
|
53
|
+
- `--description <text>` (optional)
|
|
54
|
+
- `--force` (optional, allow writing into non-empty directory)
|
|
55
|
+
|
|
56
|
+
## Rules
|
|
57
|
+
|
|
58
|
+
- Always use `_` (underscore) separators in Rust crate names.
|
|
59
|
+
- Do **not** use `-` in crate package names. Example: `acme_tool_core`, not `acme-tool-core`.
|
|
60
|
+
|
|
61
|
+
## After scaffolding
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
cd <project>
|
|
65
|
+
|
|
66
|
+
direnv allow
|
|
67
|
+
# or
|
|
68
|
+
# devenv shell
|
|
69
|
+
|
|
70
|
+
install:cargo:bin
|
|
71
|
+
build:all
|
|
72
|
+
lint:all
|
|
73
|
+
test:all
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
Create your first changeset:
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
knope document-change
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
Dry-run a release:
|
|
83
|
+
|
|
84
|
+
```bash
|
|
85
|
+
knope release --dry-run
|
|
86
|
+
```
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import fs from "node:fs";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import process from "node:process";
|
|
6
|
+
import { fileURLToPath } from "node:url";
|
|
7
|
+
|
|
8
|
+
function printUsage() {
|
|
9
|
+
console.log(`
|
|
10
|
+
Rust Workspace Bootstrap
|
|
11
|
+
|
|
12
|
+
Usage:
|
|
13
|
+
scaffold.js --name <project-name> [options]
|
|
14
|
+
|
|
15
|
+
Options:
|
|
16
|
+
--name <kebab-case> Required project name.
|
|
17
|
+
--dir <path> Target directory (default: ./<name>).
|
|
18
|
+
--owner <github-owner> GitHub org/user (default: your-github-org).
|
|
19
|
+
--repo <github-repo> GitHub repo name (default: <name>).
|
|
20
|
+
--description <text> Workspace description.
|
|
21
|
+
--force Allow writing into a non-empty target directory.
|
|
22
|
+
--help Show this help.
|
|
23
|
+
`);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function parseArgs(argv) {
|
|
27
|
+
const options = {
|
|
28
|
+
name: "",
|
|
29
|
+
dir: "",
|
|
30
|
+
owner: "your-github-org",
|
|
31
|
+
repo: "",
|
|
32
|
+
description: "",
|
|
33
|
+
force: false,
|
|
34
|
+
help: false,
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
38
|
+
const arg = argv[index];
|
|
39
|
+
if (arg === "--help" || arg === "-h") {
|
|
40
|
+
options.help = true;
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
if (arg === "--force") {
|
|
44
|
+
options.force = true;
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (arg.startsWith("--")) {
|
|
49
|
+
const key = arg.slice(2);
|
|
50
|
+
const value = argv[index + 1];
|
|
51
|
+
if (!value || value.startsWith("--")) {
|
|
52
|
+
throw new Error(`Missing value for option: ${arg}`);
|
|
53
|
+
}
|
|
54
|
+
index += 1;
|
|
55
|
+
|
|
56
|
+
switch (key) {
|
|
57
|
+
case "name":
|
|
58
|
+
options.name = value;
|
|
59
|
+
break;
|
|
60
|
+
case "dir":
|
|
61
|
+
options.dir = value;
|
|
62
|
+
break;
|
|
63
|
+
case "owner":
|
|
64
|
+
options.owner = value;
|
|
65
|
+
break;
|
|
66
|
+
case "repo":
|
|
67
|
+
options.repo = value;
|
|
68
|
+
break;
|
|
69
|
+
case "description":
|
|
70
|
+
options.description = value;
|
|
71
|
+
break;
|
|
72
|
+
default:
|
|
73
|
+
throw new Error(`Unknown option: ${arg}`);
|
|
74
|
+
}
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (!options.name) {
|
|
79
|
+
options.name = arg;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return options;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function toTitleCase(value) {
|
|
87
|
+
return value
|
|
88
|
+
.split("-")
|
|
89
|
+
.map((part) => part.charAt(0).toUpperCase() + part.slice(1))
|
|
90
|
+
.join(" ");
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function collectFiles(rootDir, currentDir = rootDir) {
|
|
94
|
+
const entries = fs.readdirSync(currentDir, { withFileTypes: true });
|
|
95
|
+
const files = [];
|
|
96
|
+
|
|
97
|
+
for (const entry of entries) {
|
|
98
|
+
const absolutePath = path.join(currentDir, entry.name);
|
|
99
|
+
if (entry.isDirectory()) {
|
|
100
|
+
files.push(...collectFiles(rootDir, absolutePath));
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
if (entry.isFile()) {
|
|
104
|
+
files.push(path.relative(rootDir, absolutePath));
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return files;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function applyTokens(input, tokens) {
|
|
112
|
+
let output = input;
|
|
113
|
+
for (const [token, value] of Object.entries(tokens)) {
|
|
114
|
+
output = output.split(token).join(value);
|
|
115
|
+
}
|
|
116
|
+
return output;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function ensureTargetDirectory(targetDir, force) {
|
|
120
|
+
if (!fs.existsSync(targetDir)) {
|
|
121
|
+
fs.mkdirSync(targetDir, { recursive: true });
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const currentFiles = fs.readdirSync(targetDir);
|
|
126
|
+
if (currentFiles.length > 0 && !force) {
|
|
127
|
+
throw new Error(
|
|
128
|
+
`Target directory is not empty: ${targetDir}\nUse --force if you want to scaffold into an existing directory.`,
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function main() {
|
|
134
|
+
const options = parseArgs(process.argv.slice(2));
|
|
135
|
+
if (options.help) {
|
|
136
|
+
printUsage();
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (!options.name) {
|
|
141
|
+
printUsage();
|
|
142
|
+
throw new Error("Project name is required. Pass --name <project-name>.");
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (!/^[a-z][a-z0-9-]*$/.test(options.name)) {
|
|
146
|
+
throw new Error("Project name must be kebab-case (letters, numbers, dashes). Example: acme-tool");
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const projectName = options.name;
|
|
150
|
+
const projectTitle = toTitleCase(projectName);
|
|
151
|
+
const cratePrefix = projectName.replace(/-/g, "_");
|
|
152
|
+
const coreCrate = `${cratePrefix}_core`;
|
|
153
|
+
const cliCrate = `${cratePrefix}_cli`;
|
|
154
|
+
const targetDir = path.resolve(options.dir || projectName);
|
|
155
|
+
const owner = options.owner;
|
|
156
|
+
const repo = options.repo || projectName;
|
|
157
|
+
const description = options.description || `${projectTitle} Rust workspace`;
|
|
158
|
+
|
|
159
|
+
const tokens = {
|
|
160
|
+
"__PROJECT_NAME__": projectName,
|
|
161
|
+
"__PROJECT_TITLE__": projectTitle,
|
|
162
|
+
"__CORE_CRATE__": coreCrate,
|
|
163
|
+
"__CLI_CRATE__": cliCrate,
|
|
164
|
+
"__GITHUB_OWNER__": owner,
|
|
165
|
+
"__GITHUB_REPO__": repo,
|
|
166
|
+
"__DESCRIPTION__": description,
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
const currentFile = fileURLToPath(import.meta.url);
|
|
170
|
+
const baseDir = path.dirname(currentFile);
|
|
171
|
+
const templateDir = path.join(baseDir, "template");
|
|
172
|
+
|
|
173
|
+
if (!fs.existsSync(templateDir)) {
|
|
174
|
+
throw new Error(`Template directory not found: ${templateDir}`);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
ensureTargetDirectory(targetDir, options.force);
|
|
178
|
+
|
|
179
|
+
const templateFiles = collectFiles(templateDir);
|
|
180
|
+
const writtenFiles = [];
|
|
181
|
+
|
|
182
|
+
for (const relativeTemplatePath of templateFiles) {
|
|
183
|
+
const sourcePath = path.join(templateDir, relativeTemplatePath);
|
|
184
|
+
const targetRelativePath = applyTokens(relativeTemplatePath, tokens);
|
|
185
|
+
const destinationPath = path.join(targetDir, targetRelativePath);
|
|
186
|
+
const sourceContent = fs.readFileSync(sourcePath, "utf8");
|
|
187
|
+
const destinationContent = applyTokens(sourceContent, tokens);
|
|
188
|
+
|
|
189
|
+
fs.mkdirSync(path.dirname(destinationPath), { recursive: true });
|
|
190
|
+
fs.writeFileSync(destinationPath, destinationContent, "utf8");
|
|
191
|
+
|
|
192
|
+
if (destinationPath.endsWith(".sh")) {
|
|
193
|
+
fs.chmodSync(destinationPath, 0o755);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
writtenFiles.push(targetRelativePath);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
writtenFiles.sort();
|
|
200
|
+
|
|
201
|
+
console.log(`\n✅ Scaffolding complete: ${targetDir}`);
|
|
202
|
+
console.log(` Project: ${projectName}`);
|
|
203
|
+
console.log(` Core crate: ${coreCrate}`);
|
|
204
|
+
console.log(` CLI crate: ${cliCrate}`);
|
|
205
|
+
console.log(` GitHub repo: ${owner}/${repo}`);
|
|
206
|
+
console.log(` Files: ${writtenFiles.length}`);
|
|
207
|
+
console.log("\nNext steps:");
|
|
208
|
+
console.log(` cd ${targetDir}`);
|
|
209
|
+
console.log(" direnv allow # or: devenv shell");
|
|
210
|
+
console.log(" install:cargo:bin");
|
|
211
|
+
console.log(" lint:all && test:all && build:all");
|
|
212
|
+
console.log(" knope document-change");
|
|
213
|
+
console.log(" knope release --dry-run\n");
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
try {
|
|
217
|
+
main();
|
|
218
|
+
} catch (error) {
|
|
219
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
220
|
+
console.error(`\n❌ ${message}\n`);
|
|
221
|
+
process.exit(1);
|
|
222
|
+
}
|
|
File without changes
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
name: devenv
|
|
2
|
+
description: Setup development environment with devenv
|
|
3
|
+
inputs:
|
|
4
|
+
github-token:
|
|
5
|
+
description: Provide a GitHub token
|
|
6
|
+
required: true
|
|
7
|
+
cache-version:
|
|
8
|
+
description: Cache version for dependency keys
|
|
9
|
+
required: false
|
|
10
|
+
default: "v1"
|
|
11
|
+
|
|
12
|
+
runs:
|
|
13
|
+
using: composite
|
|
14
|
+
steps:
|
|
15
|
+
- name: cache rust dependencies
|
|
16
|
+
uses: Swatinem/rust-cache@v2
|
|
17
|
+
with:
|
|
18
|
+
prefix-key: "${{ inputs.cache-version }}"
|
|
19
|
+
|
|
20
|
+
- name: cache cargo binaries
|
|
21
|
+
uses: actions/cache@v4
|
|
22
|
+
with:
|
|
23
|
+
path: ./.bin
|
|
24
|
+
key: ${{ runner.os }}-cargo-bin-${{ inputs.cache-version }}-${{ env.RUSTUP_TOOLCHAIN }}-${{ hashFiles('rust-toolchain.toml', 'Cargo.toml') }}
|
|
25
|
+
restore-keys: |
|
|
26
|
+
${{ runner.os }}-cargo-bin-${{ inputs.cache-version }}-
|
|
27
|
+
|
|
28
|
+
- name: install nix
|
|
29
|
+
uses: cachix/install-nix-action@v31
|
|
30
|
+
with:
|
|
31
|
+
github_access_token: ${{ inputs.github-token }}
|
|
32
|
+
|
|
33
|
+
- name: setup nix environment
|
|
34
|
+
run: |
|
|
35
|
+
nix profile add --accept-flake-config nixpkgs#cachix
|
|
36
|
+
cachix use devenv
|
|
37
|
+
nix profile add nixpkgs#devenv
|
|
38
|
+
shell: bash
|
|
39
|
+
|
|
40
|
+
- name: install dependencies
|
|
41
|
+
run: install:cargo:bin
|
|
42
|
+
shell: devenv shell -- bash -e {0}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
name: "ci"
|
|
2
|
+
permissions:
|
|
3
|
+
contents: read
|
|
4
|
+
on:
|
|
5
|
+
push:
|
|
6
|
+
branches:
|
|
7
|
+
- main
|
|
8
|
+
pull_request:
|
|
9
|
+
branches:
|
|
10
|
+
- main
|
|
11
|
+
|
|
12
|
+
concurrency:
|
|
13
|
+
group: ${{ github.workflow }}-${{ github.ref }}
|
|
14
|
+
cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}
|
|
15
|
+
|
|
16
|
+
jobs:
|
|
17
|
+
commit-lint:
|
|
18
|
+
timeout-minutes: 15
|
|
19
|
+
runs-on: ubuntu-latest
|
|
20
|
+
steps:
|
|
21
|
+
- name: checkout repository
|
|
22
|
+
uses: actions/checkout@v4
|
|
23
|
+
with:
|
|
24
|
+
fetch-depth: 0
|
|
25
|
+
|
|
26
|
+
- name: validate commit messages
|
|
27
|
+
run: |
|
|
28
|
+
PATTERN='^(feat|fix|docs|style|refactor|perf|test|build|ci|chore|revert)(\(.+\))?!?: .+'
|
|
29
|
+
|
|
30
|
+
if [ "${{ github.event_name }}" = "pull_request" ]; then
|
|
31
|
+
BASE_SHA='${{ github.event.pull_request.base.sha }}'
|
|
32
|
+
HEAD_SHA='${{ github.event.pull_request.head.sha }}'
|
|
33
|
+
COMMITS=$(git log --format='%s' --no-merges "${BASE_SHA}..${HEAD_SHA}")
|
|
34
|
+
else
|
|
35
|
+
COMMITS=$(git log --format='%s' -1)
|
|
36
|
+
fi
|
|
37
|
+
|
|
38
|
+
FAILED=0
|
|
39
|
+
while IFS= read -r msg; do
|
|
40
|
+
[ -z "$msg" ] && continue
|
|
41
|
+
if ! echo "$msg" | grep -qE "$PATTERN"; then
|
|
42
|
+
echo "::error::Invalid commit message: '$msg'"
|
|
43
|
+
FAILED=1
|
|
44
|
+
fi
|
|
45
|
+
done <<< "$COMMITS"
|
|
46
|
+
|
|
47
|
+
if [ "$FAILED" -eq 1 ]; then
|
|
48
|
+
exit 1
|
|
49
|
+
fi
|
|
50
|
+
|
|
51
|
+
changeset:
|
|
52
|
+
timeout-minutes: 10
|
|
53
|
+
runs-on: ubuntu-latest
|
|
54
|
+
if: github.event_name == 'pull_request' || !startsWith(github.event.head_commit.message, 'chore: prepare releases')
|
|
55
|
+
steps:
|
|
56
|
+
- name: checkout repository
|
|
57
|
+
uses: actions/checkout@v4
|
|
58
|
+
|
|
59
|
+
- name: check for changeset
|
|
60
|
+
run: |
|
|
61
|
+
CHANGESETS=$(find .changeset -name '*.md' ! -name 'README.md' 2>/dev/null | wc -l | tr -d ' ')
|
|
62
|
+
if [ "$CHANGESETS" -eq 0 ]; then
|
|
63
|
+
echo "::error::No changeset found. Run 'knope document-change'."
|
|
64
|
+
exit 1
|
|
65
|
+
fi
|
|
66
|
+
echo "✅ Found $CHANGESETS changeset(s)"
|
|
67
|
+
|
|
68
|
+
lint:
|
|
69
|
+
timeout-minutes: 60
|
|
70
|
+
runs-on: ubuntu-latest
|
|
71
|
+
steps:
|
|
72
|
+
- name: checkout repository
|
|
73
|
+
uses: actions/checkout@v4
|
|
74
|
+
|
|
75
|
+
- name: setup
|
|
76
|
+
uses: ./.github/actions/devenv
|
|
77
|
+
with:
|
|
78
|
+
github-token: ${{ secrets.GITHUB_TOKEN }}
|
|
79
|
+
|
|
80
|
+
- name: lint
|
|
81
|
+
run: lint:all
|
|
82
|
+
shell: devenv shell -c -- bash -e {0}
|
|
83
|
+
|
|
84
|
+
test:
|
|
85
|
+
timeout-minutes: 60
|
|
86
|
+
runs-on: ubuntu-latest
|
|
87
|
+
steps:
|
|
88
|
+
- name: checkout repository
|
|
89
|
+
uses: actions/checkout@v4
|
|
90
|
+
|
|
91
|
+
- name: setup
|
|
92
|
+
uses: ./.github/actions/devenv
|
|
93
|
+
with:
|
|
94
|
+
github-token: ${{ secrets.GITHUB_TOKEN }}
|
|
95
|
+
|
|
96
|
+
- name: run tests
|
|
97
|
+
run: test:all
|
|
98
|
+
shell: devenv shell -c -- bash -e {0}
|
|
99
|
+
|
|
100
|
+
build:
|
|
101
|
+
timeout-minutes: 60
|
|
102
|
+
runs-on: ubuntu-latest
|
|
103
|
+
needs: [commit-lint, changeset, lint, test]
|
|
104
|
+
if: ${{ !failure() && !cancelled() }}
|
|
105
|
+
steps:
|
|
106
|
+
- name: checkout repository
|
|
107
|
+
uses: actions/checkout@v4
|
|
108
|
+
|
|
109
|
+
- name: setup
|
|
110
|
+
uses: ./.github/actions/devenv
|
|
111
|
+
with:
|
|
112
|
+
github-token: ${{ secrets.GITHUB_TOKEN }}
|
|
113
|
+
|
|
114
|
+
- name: build default features
|
|
115
|
+
run: build:default
|
|
116
|
+
shell: devenv shell -c -- bash -e {0}
|
|
117
|
+
|
|
118
|
+
- name: build all features
|
|
119
|
+
run: build:all
|
|
120
|
+
shell: devenv shell -c -- bash -e {0}
|
|
121
|
+
|
|
122
|
+
security:
|
|
123
|
+
timeout-minutes: 30
|
|
124
|
+
runs-on: ubuntu-latest
|
|
125
|
+
steps:
|
|
126
|
+
- name: checkout repository
|
|
127
|
+
uses: actions/checkout@v4
|
|
128
|
+
|
|
129
|
+
- name: run cargo-deny
|
|
130
|
+
uses: EmbarkStudios/cargo-deny-action@v2
|