@robineb/project-cli 1.0.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 +145 -0
- package/dist/index.js +347 -0
- package/dist/templates/esp32/.gitattributes +1 -0
- package/dist/templates/esp32/.github/workflows/build.yml +31 -0
- package/dist/templates/esp32/README.md +30 -0
- package/dist/templates/esp32/gitignore +37 -0
- package/dist/templates/esp32/include/README +39 -0
- package/dist/templates/esp32/lib/README +46 -0
- package/dist/templates/esp32/platformio.ini +39 -0
- package/dist/templates/esp32/src/main.cpp +10 -0
- package/dist/templates/esp32/test/README +11 -0
- package/dist/templates/ts-lib/README.md +6 -0
- package/dist/templates/ts-lib/package.json +14 -0
- package/dist/templates/ts-lib/src/index.ts +1 -0
- package/dist/templates/ts-lib/tsconfig.json +13 -0
- package/package.json +41 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Robineb
|
|
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,145 @@
|
|
|
1
|
+
# project-cli
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/@robineb/project-cli)
|
|
4
|
+
|
|
5
|
+
Persönliches CLI-Tool, das neue Projekte anlegt: Grundgerüst erzeugen, Git
|
|
6
|
+
initialisieren, optional ein Remote-Repository auf GitHub oder Gitea
|
|
7
|
+
anlegen und pushen. Läuft unter Windows und macOS.
|
|
8
|
+
|
|
9
|
+
## Installation
|
|
10
|
+
|
|
11
|
+
```
|
|
12
|
+
npm i -g @robineb/project-cli
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
(Package auf [npmjs.com](https://www.npmjs.com/package/@robineb/project-cli),
|
|
16
|
+
anonym installierbar – kein Account/Token nötig. Scoped, weil der
|
|
17
|
+
naheliegende unscoped Name `project-cli` bereits an ein fremdes Paket
|
|
18
|
+
vergeben ist. Der aufgerufene Befehl heißt trotzdem schlicht `project-cli`,
|
|
19
|
+
das bestimmt der `bin`-Eintrag, nicht der Package-Name. Zusätzlich als
|
|
20
|
+
`@robin1053/project-cli` auf GitHub Packages veröffentlicht, rein für die
|
|
21
|
+
Sichtbarkeit im Repo; installieren lohnt sich darüber nur, wenn man ohnehin
|
|
22
|
+
schon für GitHub Packages authentifiziert ist.)
|
|
23
|
+
|
|
24
|
+
## Voraussetzungen
|
|
25
|
+
|
|
26
|
+
- Node.js 26+ (das Tool nutzt Node's native, Strip-only TypeScript-Ausführung –
|
|
27
|
+
kein Build-Schritt nötig)
|
|
28
|
+
- Git
|
|
29
|
+
- Für den `esp32`-Stack zusätzlich [PlatformIO Core](https://docs.platformio.org/en/latest/core/installation/index.html) (`pio`)
|
|
30
|
+
|
|
31
|
+
## Benutzung
|
|
32
|
+
|
|
33
|
+
```
|
|
34
|
+
node index.ts [name] [--private]
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Ohne Argumente führt `node index.ts` interaktiv durch:
|
|
38
|
+
|
|
39
|
+
1. Projektname
|
|
40
|
+
2. Stack (`next`, `ts-lib`, `esp32`, …)
|
|
41
|
+
3. Remote-Repository: keins / GitHub / Gitea
|
|
42
|
+
4. Grundgerüst wird erzeugt, Git initialisiert und der erste Commit gemacht
|
|
43
|
+
5. Bei Remote-Wahl: Repo wird angelegt und gepusht
|
|
44
|
+
|
|
45
|
+
`-p, --private` legt ein angelegtes Remote-Repo privat statt öffentlich an.
|
|
46
|
+
|
|
47
|
+
Bricht ab, wenn der Zielordner bereits existiert.
|
|
48
|
+
|
|
49
|
+
### Stacks
|
|
50
|
+
|
|
51
|
+
| Stack | Beschreibung |
|
|
52
|
+
|---|---|
|
|
53
|
+
| `next` | Next.js (TypeScript, App Router) via `create-next-app` |
|
|
54
|
+
| `ts-lib` | Eigenes TypeScript-Package-Template |
|
|
55
|
+
| `esp32` | PlatformIO/ESP32-Projekt (Arduino-Framework) |
|
|
56
|
+
|
|
57
|
+
Neue Stacks werden einfach als Eintrag in `STACKS` in `index.ts` ergänzt –
|
|
58
|
+
entweder mit `command` (delegiert an einen externen Generator) oder mit
|
|
59
|
+
`template` (kopiert einen Ordner aus `templates/`).
|
|
60
|
+
|
|
61
|
+
### PlatformIO-Befehle (für `esp32`-Projekte)
|
|
62
|
+
|
|
63
|
+
Im Projektordner eines gescaffoldeten `esp32`-Projekts ausgeführt:
|
|
64
|
+
|
|
65
|
+
```
|
|
66
|
+
node <pfad-zu-project-cli>/index.ts build [env]
|
|
67
|
+
node <pfad-zu-project-cli>/index.ts upload [env]
|
|
68
|
+
node <pfad-zu-project-cli>/index.ts add-board
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
- `build` – Firmware bauen (`pio run`)
|
|
72
|
+
- `upload` – Firmware bauen und aufs Board flashen (`pio run -t upload`)
|
|
73
|
+
- `add-board` – neues ESP32-Board suchen (`pio boards espressif32`) und als
|
|
74
|
+
`[env:...]`-Sektion zur `platformio.ini` hinzufügen
|
|
75
|
+
|
|
76
|
+
`env` ist optional – ohne Angabe wird interaktiv aus den in `platformio.ini`
|
|
77
|
+
definierten `[env:...]`-Sektionen ausgewählt.
|
|
78
|
+
|
|
79
|
+
### Remote-Repos & Zugangsdaten
|
|
80
|
+
|
|
81
|
+
GitHub- bzw. Gitea-Token werden in dieser Reihenfolge aufgelöst:
|
|
82
|
+
|
|
83
|
+
1. Umgebungsvariable (`GITHUB_TOKEN` bzw. `GITEA_URL`/`GITEA_TOKEN`)
|
|
84
|
+
2. gespeicherter Wert
|
|
85
|
+
3. interaktive Abfrage (wird danach für nächstes Mal gespeichert)
|
|
86
|
+
|
|
87
|
+
Tokens landen dabei im OS-eigenen Schlüsselbund über [`@napi-rs/keyring`](https://github.com/napi-rs/keyring-node)
|
|
88
|
+
(Windows Credential Manager, macOS Keychain, Linux Secret Service/libsecret) –
|
|
89
|
+
nicht in einer Klartextdatei. Die Gitea-URL (kein Geheimnis) bleibt plattformgerecht
|
|
90
|
+
in [`conf`](https://github.com/sindresorhus/conf) (`%APPDATA%` unter Windows,
|
|
91
|
+
`~/Library/Application Support` unter macOS, XDG-Pfade unter Linux).
|
|
92
|
+
|
|
93
|
+
Token-Scopes: Gitea braucht `write:repository`. GitHub braucht „Administration"
|
|
94
|
+
(Schreibrecht) auf einem Fine-grained PAT, oder `repo`/`public_repo` auf
|
|
95
|
+
einem Classic PAT.
|
|
96
|
+
|
|
97
|
+
## Entwicklung
|
|
98
|
+
|
|
99
|
+
Typecheck gegen die Root-`tsconfig.json`:
|
|
100
|
+
|
|
101
|
+
```
|
|
102
|
+
npx tsc --noEmit
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
Build für die globale Installation (kompiliert nach `dist/` und kopiert
|
|
106
|
+
`templates/` dorthin, siehe `bin` in `package.json`):
|
|
107
|
+
|
|
108
|
+
```
|
|
109
|
+
npm run build
|
|
110
|
+
npm i -g .
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
Es gibt noch keine Tests.
|
|
114
|
+
|
|
115
|
+
### Veröffentlichen
|
|
116
|
+
|
|
117
|
+
`.github/workflows/publish.yaml` läuft bei jedem GitHub Release (`types:
|
|
118
|
+
[published]`) und veröffentlicht die gebaute Version zweimal parallel:
|
|
119
|
+
|
|
120
|
+
- als `@robineb/project-cli` auf npmjs.com via **Trusted Publishing** (OIDC) –
|
|
121
|
+
kein Secret im Repo, npm tauscht den GitHub-Actions-OIDC-Token automatisch
|
|
122
|
+
gegen einen kurzlebigen Publish-Token. (Klassische Access-Tokens mit
|
|
123
|
+
direktem Publish-Recht werden von npm ab Januar 2027 abgeschafft –
|
|
124
|
+
Trusted Publishing ist der empfohlene Ersatz.)
|
|
125
|
+
- als `@robin1053/project-cli` auf GitHub Packages (Auth über das
|
|
126
|
+
eingebaute `GITHUB_TOKEN`, kein Secret nötig)
|
|
127
|
+
|
|
128
|
+
Trusted Publishing lässt sich laut npm erst einrichten, wenn das Paket
|
|
129
|
+
mindestens einmal existiert – deshalb einmaliger manueller Bootstrap, danach
|
|
130
|
+
läuft alles Weitere über die CI ohne jedes Secret:
|
|
131
|
+
|
|
132
|
+
```
|
|
133
|
+
npm run build
|
|
134
|
+
npm login
|
|
135
|
+
npm publish
|
|
136
|
+
npm trust github --allow-publish --file publish.yaml
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
Ablauf für jede weitere Version: `version` in `package.json` erhöhen,
|
|
140
|
+
committen, Git-Tag + GitHub Release mit dieser Version anlegen – der
|
|
141
|
+
Workflow übernimmt den Rest.
|
|
142
|
+
|
|
143
|
+
## Lizenz
|
|
144
|
+
|
|
145
|
+
[MIT](./LICENSE)
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,347 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import fs from "node:fs/promises";
|
|
4
|
+
import { Command } from "commander";
|
|
5
|
+
import * as p from "@clack/prompts";
|
|
6
|
+
import { execa } from "execa";
|
|
7
|
+
import { simpleGit, CheckRepoActions } from "simple-git";
|
|
8
|
+
import Conf from "conf";
|
|
9
|
+
import { Entry } from "@napi-rs/keyring";
|
|
10
|
+
const STACKS = [
|
|
11
|
+
{
|
|
12
|
+
id: "next",
|
|
13
|
+
label: "Next.js (TypeScript, App Router)",
|
|
14
|
+
command: (name) => ({
|
|
15
|
+
file: "npx",
|
|
16
|
+
args: ["create-next-app@latest", name, "--ts", "--app", "--yes"],
|
|
17
|
+
}),
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
id: "ts-lib",
|
|
21
|
+
label: "TypeScript Package (eigenes Template)",
|
|
22
|
+
template: "ts-lib",
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
id: "esp32",
|
|
26
|
+
label: "PlatformIO / ESP32 (Arduino)",
|
|
27
|
+
template: "esp32",
|
|
28
|
+
},
|
|
29
|
+
// TODO: hier deine weiteren Stacks eintragen (python, ...)
|
|
30
|
+
];
|
|
31
|
+
class GiteaProvider {
|
|
32
|
+
baseUrl;
|
|
33
|
+
token;
|
|
34
|
+
constructor(baseUrl, token) {
|
|
35
|
+
this.baseUrl = baseUrl;
|
|
36
|
+
this.token = token;
|
|
37
|
+
}
|
|
38
|
+
async createRepo(name, isPrivate) {
|
|
39
|
+
const res = await fetch(`${this.baseUrl}/api/v1/user/repos`, {
|
|
40
|
+
method: "POST",
|
|
41
|
+
headers: {
|
|
42
|
+
Authorization: `token ${this.token}`,
|
|
43
|
+
"Content-Type": "application/json",
|
|
44
|
+
Accept: "application/json",
|
|
45
|
+
},
|
|
46
|
+
// auto_init: false -> leeres Repo, sonst scheitert der Push
|
|
47
|
+
body: JSON.stringify({ name, private: isPrivate, auto_init: false }),
|
|
48
|
+
});
|
|
49
|
+
if (res.status === 409)
|
|
50
|
+
throw new Error(`Repo "${name}" existiert bereits.`);
|
|
51
|
+
if (!res.ok)
|
|
52
|
+
throw new Error(`Gitea ${res.status}: ${await res.text()}`);
|
|
53
|
+
const repo = (await res.json());
|
|
54
|
+
return repo.clone_url;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
class GitHubProvider {
|
|
58
|
+
token;
|
|
59
|
+
constructor(token) {
|
|
60
|
+
this.token = token;
|
|
61
|
+
}
|
|
62
|
+
async createRepo(name, isPrivate) {
|
|
63
|
+
const res = await fetch("https://api.github.com/user/repos", {
|
|
64
|
+
method: "POST",
|
|
65
|
+
headers: {
|
|
66
|
+
Authorization: `Bearer ${this.token}`,
|
|
67
|
+
Accept: "application/vnd.github+json",
|
|
68
|
+
"X-GitHub-Api-Version": "2026-03-10",
|
|
69
|
+
"Content-Type": "application/json",
|
|
70
|
+
},
|
|
71
|
+
body: JSON.stringify({ name, private: isPrivate, auto_init: false }),
|
|
72
|
+
});
|
|
73
|
+
// GitHub liefert 422 sowohl bei Namenskollision als auch bei Validierungsfehlern
|
|
74
|
+
if (res.status === 422) {
|
|
75
|
+
throw new Error(`Repo "${name}" existiert wohl schon.`);
|
|
76
|
+
}
|
|
77
|
+
if (!res.ok)
|
|
78
|
+
throw new Error(`GitHub ${res.status}: ${await res.text()}`);
|
|
79
|
+
const repo = (await res.json());
|
|
80
|
+
return repo.clone_url;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
const config = new Conf({ projectName: "project-cli" });
|
|
84
|
+
const KEYRING_SERVICE = "project-cli";
|
|
85
|
+
function getStoredSecret(account) {
|
|
86
|
+
return new Entry(KEYRING_SERVICE, account).getPassword() ?? undefined;
|
|
87
|
+
}
|
|
88
|
+
function setStoredSecret(account, value) {
|
|
89
|
+
new Entry(KEYRING_SERVICE, account).setPassword(value);
|
|
90
|
+
}
|
|
91
|
+
async function ask(message) {
|
|
92
|
+
const answer = await p.password({ message });
|
|
93
|
+
if (p.isCancel(answer))
|
|
94
|
+
throw new Error("Abgebrochen.");
|
|
95
|
+
return answer;
|
|
96
|
+
}
|
|
97
|
+
async function askText(message) {
|
|
98
|
+
const answer = await p.text({ message });
|
|
99
|
+
if (p.isCancel(answer))
|
|
100
|
+
throw new Error("Abgebrochen.");
|
|
101
|
+
return answer;
|
|
102
|
+
}
|
|
103
|
+
async function resolveGitHubToken() {
|
|
104
|
+
const existing = process.env.GITHUB_TOKEN ?? getStoredSecret("github-token");
|
|
105
|
+
if (existing)
|
|
106
|
+
return existing;
|
|
107
|
+
const token = await ask("GitHub Personal Access Token (repo-Scope)");
|
|
108
|
+
setStoredSecret("github-token", token);
|
|
109
|
+
return token;
|
|
110
|
+
}
|
|
111
|
+
async function resolveGiteaCredentials() {
|
|
112
|
+
const existingUrl = process.env.GITEA_URL ?? config.get("giteaUrl");
|
|
113
|
+
const baseUrl = existingUrl ??
|
|
114
|
+
(await askText("Gitea-URL (z.B. https://gitea.example.com)"));
|
|
115
|
+
if (!existingUrl)
|
|
116
|
+
config.set("giteaUrl", baseUrl);
|
|
117
|
+
const existingToken = process.env.GITEA_TOKEN ?? getStoredSecret("gitea-token");
|
|
118
|
+
const token = existingToken ?? (await ask("Gitea Access Token (write:repository-Scope)"));
|
|
119
|
+
if (!existingToken)
|
|
120
|
+
setStoredSecret("gitea-token", token);
|
|
121
|
+
return [baseUrl, token];
|
|
122
|
+
}
|
|
123
|
+
// ---------------------------------------------------------------------------
|
|
124
|
+
// 3. Die einzelnen Arbeitsschritte
|
|
125
|
+
// ---------------------------------------------------------------------------
|
|
126
|
+
async function scaffold(stack, targetDir, name) {
|
|
127
|
+
if (stack.command) {
|
|
128
|
+
const { file, args } = stack.command(name);
|
|
129
|
+
// cwd = Elternordner, weil der Generator den Zielordner selbst anlegt
|
|
130
|
+
await execa(file, args, { cwd: path.dirname(targetDir), stdio: "inherit" });
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
if (stack.template) {
|
|
134
|
+
const src = path.join(import.meta.dirname, "templates", stack.template);
|
|
135
|
+
await fs.cp(src, targetDir, { recursive: true });
|
|
136
|
+
await restoreGitignore(targetDir);
|
|
137
|
+
await replacePlaceholders(targetDir, { projectName: name });
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
throw new Error(`Stack ${stack.id} hat weder command noch template`);
|
|
141
|
+
}
|
|
142
|
+
// npm streicht jede Datei, die exakt ".gitignore" heißt, aus jedem Package
|
|
143
|
+
// (unabhängig vom Pfad) -> Templates lagern sie ohne Punkt als "gitignore"
|
|
144
|
+
// und wir benennen sie hier, nach dem Kopieren ins neue Projekt, zurück um.
|
|
145
|
+
async function restoreGitignore(targetDir) {
|
|
146
|
+
const from = path.join(targetDir, "gitignore");
|
|
147
|
+
const to = path.join(targetDir, ".gitignore");
|
|
148
|
+
await fs.rename(from, to).catch(() => { });
|
|
149
|
+
}
|
|
150
|
+
async function replacePlaceholders(dir, vars) {
|
|
151
|
+
const entries = await fs.readdir(dir, {
|
|
152
|
+
withFileTypes: true,
|
|
153
|
+
recursive: true,
|
|
154
|
+
});
|
|
155
|
+
for (const entry of entries) {
|
|
156
|
+
if (!entry.isFile())
|
|
157
|
+
continue;
|
|
158
|
+
const full = path.join(entry.parentPath, entry.name);
|
|
159
|
+
let content;
|
|
160
|
+
try {
|
|
161
|
+
content = await fs.readFile(full, "utf8");
|
|
162
|
+
}
|
|
163
|
+
catch {
|
|
164
|
+
continue; // Binärdatei o.ä. -> überspringen
|
|
165
|
+
}
|
|
166
|
+
let replaced = content;
|
|
167
|
+
for (const [key, value] of Object.entries(vars)) {
|
|
168
|
+
replaced = replaced.replaceAll(`{{${key}}}`, value);
|
|
169
|
+
}
|
|
170
|
+
if (replaced !== content)
|
|
171
|
+
await fs.writeFile(full, replaced, "utf8");
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
async function initGit(targetDir) {
|
|
175
|
+
const git = simpleGit(targetDir);
|
|
176
|
+
// IS_REPO_ROOT statt Default: Default prüft nur "irgendwo unter einem Repo",
|
|
177
|
+
// das wäre auch true, wenn targetDir zufällig innerhalb eines fremden
|
|
178
|
+
// Repos liegt -> init() würde übersprungen und add/commit liefen gegen
|
|
179
|
+
// das falsche (übergeordnete) Repo. Hier soll nur erkannt werden, ob
|
|
180
|
+
// targetDir selbst schon ein eigenes .git hat (z.B. von create-next-app).
|
|
181
|
+
const alreadyRepo = await git
|
|
182
|
+
.checkIsRepo(CheckRepoActions.IS_REPO_ROOT)
|
|
183
|
+
.catch(() => false);
|
|
184
|
+
if (!alreadyRepo)
|
|
185
|
+
await git.init();
|
|
186
|
+
await git.add(".");
|
|
187
|
+
await git.commit("chore: initial scaffold");
|
|
188
|
+
}
|
|
189
|
+
// ---------------------------------------------------------------------------
|
|
190
|
+
// 3b. PlatformIO-Helfer (build/upload/add-board)
|
|
191
|
+
// Ersatz für das commands.sh-Script aus dem esp32-Template-Vorbild:
|
|
192
|
+
// dort Bash + fzf, hier execa + @clack/prompts, damit es auch unter
|
|
193
|
+
// PowerShell/cmd läuft. Arbeiten immer auf der platformio.ini im cwd.
|
|
194
|
+
// ---------------------------------------------------------------------------
|
|
195
|
+
async function readPioEnvironments(cwd) {
|
|
196
|
+
const iniPath = path.join(cwd, "platformio.ini");
|
|
197
|
+
let content;
|
|
198
|
+
try {
|
|
199
|
+
content = await fs.readFile(iniPath, "utf8");
|
|
200
|
+
}
|
|
201
|
+
catch {
|
|
202
|
+
throw new Error(`Keine platformio.ini in ${cwd} gefunden.`);
|
|
203
|
+
}
|
|
204
|
+
return [...content.matchAll(/^\[env:([^\]]+)\]/gm)].map((m) => m[1]);
|
|
205
|
+
}
|
|
206
|
+
async function selectPioEnvironment(cwd, message) {
|
|
207
|
+
const envs = await readPioEnvironments(cwd);
|
|
208
|
+
if (envs.length === 0)
|
|
209
|
+
throw new Error("Keine [env:...]-Sektionen in platformio.ini gefunden.");
|
|
210
|
+
if (envs.length === 1)
|
|
211
|
+
return envs[0];
|
|
212
|
+
const choice = await p.select({
|
|
213
|
+
message,
|
|
214
|
+
options: envs.map((e) => ({ value: e, label: e })),
|
|
215
|
+
});
|
|
216
|
+
if (p.isCancel(choice))
|
|
217
|
+
throw new Error("Abgebrochen.");
|
|
218
|
+
return choice;
|
|
219
|
+
}
|
|
220
|
+
async function searchEsp32Boards(query) {
|
|
221
|
+
const { stdout } = await execa("pio", [
|
|
222
|
+
"boards",
|
|
223
|
+
"espressif32",
|
|
224
|
+
"--json-output",
|
|
225
|
+
]);
|
|
226
|
+
const boards = JSON.parse(stdout);
|
|
227
|
+
if (!query.trim())
|
|
228
|
+
return boards;
|
|
229
|
+
const q = query.toLowerCase();
|
|
230
|
+
return boards.filter((b) => `${b.id} ${b.name}`.toLowerCase().includes(q));
|
|
231
|
+
}
|
|
232
|
+
async function addBoard(cwd) {
|
|
233
|
+
const query = await askText('Board suchen (z.B. "esp32-s3", leer = alle anzeigen)');
|
|
234
|
+
const boards = await searchEsp32Boards(query);
|
|
235
|
+
if (boards.length === 0)
|
|
236
|
+
throw new Error("Keine passenden Boards gefunden.");
|
|
237
|
+
const boardId = await p.select({
|
|
238
|
+
message: "Welches Board?",
|
|
239
|
+
options: boards
|
|
240
|
+
.slice(0, 50)
|
|
241
|
+
.map((b) => ({ value: b.id, label: `${b.name} (${b.id})` })),
|
|
242
|
+
});
|
|
243
|
+
if (p.isCancel(boardId))
|
|
244
|
+
throw new Error("Abgebrochen.");
|
|
245
|
+
const envName = await askText('Name der neuen Environment (z.B. "my-esp32-s3")');
|
|
246
|
+
const section = `\n[env:${envName}]\nplatform = espressif32\nboard = ${boardId}\n`;
|
|
247
|
+
await fs.appendFile(path.join(cwd, "platformio.ini"), section, "utf8");
|
|
248
|
+
}
|
|
249
|
+
// ---------------------------------------------------------------------------
|
|
250
|
+
// 4. Der eigentliche Ablauf
|
|
251
|
+
// ---------------------------------------------------------------------------
|
|
252
|
+
async function run(nameArg, opts) {
|
|
253
|
+
p.intro("Projekt-Setup");
|
|
254
|
+
const name = nameArg ??
|
|
255
|
+
(await p.text({
|
|
256
|
+
message: "Wie soll das Projekt heißen?",
|
|
257
|
+
validate: (v) => (v?.trim() ? undefined : "Name darf nicht leer sein"),
|
|
258
|
+
}));
|
|
259
|
+
if (p.isCancel(name))
|
|
260
|
+
return p.cancel("Abgebrochen.");
|
|
261
|
+
const stackId = await p.select({
|
|
262
|
+
message: "Welcher Stack?",
|
|
263
|
+
options: STACKS.map((s) => ({ value: s.id, label: s.label })),
|
|
264
|
+
});
|
|
265
|
+
if (p.isCancel(stackId))
|
|
266
|
+
return p.cancel("Abgebrochen.");
|
|
267
|
+
const remoteChoice = await p.select({
|
|
268
|
+
message: "Remote-Repository anlegen?",
|
|
269
|
+
options: [
|
|
270
|
+
{ value: "none", label: "Nein, nur lokal" },
|
|
271
|
+
{ value: "github", label: "GitHub" },
|
|
272
|
+
{ value: "gitea", label: "Gitea" },
|
|
273
|
+
],
|
|
274
|
+
});
|
|
275
|
+
if (p.isCancel(remoteChoice))
|
|
276
|
+
return p.cancel("Abgebrochen.");
|
|
277
|
+
const stack = STACKS.find((s) => s.id === stackId);
|
|
278
|
+
const targetDir = path.resolve(process.cwd(), name);
|
|
279
|
+
// Nicht in einen vorhandenen Ordner schreiben
|
|
280
|
+
const exists = await fs.stat(targetDir).then(() => true, () => false);
|
|
281
|
+
if (exists) {
|
|
282
|
+
p.cancel(`Ordner "${name}" existiert bereits.`);
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
const s = p.spinner();
|
|
286
|
+
s.start("Grundgerüst wird angelegt");
|
|
287
|
+
await scaffold(stack, targetDir, name);
|
|
288
|
+
s.stop("Grundgerüst steht");
|
|
289
|
+
s.start("Git wird initialisiert");
|
|
290
|
+
await initGit(targetDir);
|
|
291
|
+
s.stop("Erster Commit ist da");
|
|
292
|
+
if (remoteChoice !== "none") {
|
|
293
|
+
// Zugangsdaten zuerst einsammeln (interaktiv, falls nötig) — danach erst
|
|
294
|
+
// den Spinner starten, sonst überlagern sich Prompt und Spinner.
|
|
295
|
+
const provider = remoteChoice === "gitea"
|
|
296
|
+
? new GiteaProvider(...(await resolveGiteaCredentials()))
|
|
297
|
+
: new GitHubProvider(await resolveGitHubToken());
|
|
298
|
+
s.start("Remote wird angelegt");
|
|
299
|
+
const cloneUrl = await provider.createRepo(name, opts.private ?? false);
|
|
300
|
+
const git = simpleGit(targetDir);
|
|
301
|
+
await git.addRemote("origin", cloneUrl);
|
|
302
|
+
await git.push(["-u", "origin", "HEAD"]);
|
|
303
|
+
s.stop(`Gepusht nach ${cloneUrl}`);
|
|
304
|
+
}
|
|
305
|
+
p.outro(`Fertig. cd ${name}`);
|
|
306
|
+
}
|
|
307
|
+
// ---------------------------------------------------------------------------
|
|
308
|
+
// 5. Einstiegspunkt
|
|
309
|
+
// ---------------------------------------------------------------------------
|
|
310
|
+
const program = new Command();
|
|
311
|
+
program
|
|
312
|
+
.name("myinit")
|
|
313
|
+
.description("Legt neue Projekte inkl. Repo an")
|
|
314
|
+
.argument("[name]", "Projektname")
|
|
315
|
+
.option("-p, --private", "Remote-Repo privat anlegen")
|
|
316
|
+
.action(run);
|
|
317
|
+
// Die folgenden Befehle arbeiten auf einem bestehenden PlatformIO-Projekt
|
|
318
|
+
// im aktuellen Arbeitsverzeichnis (nicht auf dem Scaffold-Flow oben).
|
|
319
|
+
program
|
|
320
|
+
.command("build [env]")
|
|
321
|
+
.description("PlatformIO-Firmware bauen (pio run)")
|
|
322
|
+
.action(async (env) => {
|
|
323
|
+
const cwd = process.cwd();
|
|
324
|
+
const selected = env ?? (await selectPioEnvironment(cwd, "Welche Environment bauen?"));
|
|
325
|
+
await execa("pio", ["run", "-e", selected], { cwd, stdio: "inherit" });
|
|
326
|
+
});
|
|
327
|
+
program
|
|
328
|
+
.command("upload [env]")
|
|
329
|
+
.description("Firmware bauen und aufs Board flashen (pio run -t upload)")
|
|
330
|
+
.action(async (env) => {
|
|
331
|
+
const cwd = process.cwd();
|
|
332
|
+
const selected = env ?? (await selectPioEnvironment(cwd, "Welche Environment flashen?"));
|
|
333
|
+
await execa("pio", ["run", "-e", selected, "-t", "upload"], {
|
|
334
|
+
cwd,
|
|
335
|
+
stdio: "inherit",
|
|
336
|
+
});
|
|
337
|
+
});
|
|
338
|
+
program
|
|
339
|
+
.command("add-board")
|
|
340
|
+
.description("Neues ESP32-Board zur platformio.ini hinzufügen")
|
|
341
|
+
.action(async () => {
|
|
342
|
+
await addBoard(process.cwd());
|
|
343
|
+
});
|
|
344
|
+
program.parseAsync().catch((err) => {
|
|
345
|
+
console.error(err instanceof Error ? err.message : err);
|
|
346
|
+
process.exit(1);
|
|
347
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
* text=auto
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
name: PlatformIO CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
paths:
|
|
6
|
+
- 'include/**'
|
|
7
|
+
- 'src/**'
|
|
8
|
+
- 'lib/**'
|
|
9
|
+
- 'test/**'
|
|
10
|
+
- 'platformio.ini'
|
|
11
|
+
|
|
12
|
+
jobs:
|
|
13
|
+
build:
|
|
14
|
+
runs-on: ubuntu-latest
|
|
15
|
+
|
|
16
|
+
steps:
|
|
17
|
+
- uses: actions/checkout@v3
|
|
18
|
+
- uses: actions/cache@v3
|
|
19
|
+
with:
|
|
20
|
+
path: |
|
|
21
|
+
~/.cache/pip
|
|
22
|
+
~/.platformio/.cache
|
|
23
|
+
key: ${{ runner.os }}-pio
|
|
24
|
+
- uses: actions/setup-python@v4
|
|
25
|
+
with:
|
|
26
|
+
python-version: '3.9'
|
|
27
|
+
- name: Install PlatformIO Core
|
|
28
|
+
run: pip install --upgrade platformio
|
|
29
|
+
|
|
30
|
+
- name: Build PlatformIO Project
|
|
31
|
+
run: pio run
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# {{projectName}}
|
|
2
|
+
|
|
3
|
+
PlatformIO-Projekt für ESP32 (Arduino-Framework).
|
|
4
|
+
|
|
5
|
+
## Setup
|
|
6
|
+
|
|
7
|
+
1. [PlatformIO Core](https://docs.platformio.org/en/latest/core/installation/index.html) installieren.
|
|
8
|
+
2. Board per USB anschließen.
|
|
9
|
+
|
|
10
|
+
## Bauen & Flashen
|
|
11
|
+
|
|
12
|
+
Über project-cli (im Projektordner ausgeführt):
|
|
13
|
+
|
|
14
|
+
node <pfad-zu-project-cli>/index.ts build [env]
|
|
15
|
+
node <pfad-zu-project-cli>/index.ts upload [env]
|
|
16
|
+
node <pfad-zu-project-cli>/index.ts add-board
|
|
17
|
+
|
|
18
|
+
`env` ist optional — ohne Angabe wird interaktiv aus den in `platformio.ini`
|
|
19
|
+
definierten `[env:...]`-Sektionen ausgewählt.
|
|
20
|
+
|
|
21
|
+
Oder direkt über PlatformIO:
|
|
22
|
+
|
|
23
|
+
pio run -e esp32dev
|
|
24
|
+
pio run -e esp32dev -t upload
|
|
25
|
+
pio device monitor
|
|
26
|
+
|
|
27
|
+
## Boards
|
|
28
|
+
|
|
29
|
+
Vordefiniert: `esp32dev`, `featheresp32`, `adafruit_feather_esp32s2`.
|
|
30
|
+
Weitere über `add-board` oder manuell in `platformio.ini` ergänzen.
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# Prerequisites
|
|
2
|
+
*.d
|
|
3
|
+
|
|
4
|
+
# Compiled Object files
|
|
5
|
+
*.slo
|
|
6
|
+
*.lo
|
|
7
|
+
*.o
|
|
8
|
+
*.obj
|
|
9
|
+
|
|
10
|
+
# Precompiled Headers
|
|
11
|
+
*.gch
|
|
12
|
+
*.pch
|
|
13
|
+
|
|
14
|
+
# Compiled Dynamic libraries
|
|
15
|
+
*.so
|
|
16
|
+
*.dylib
|
|
17
|
+
*.dll
|
|
18
|
+
|
|
19
|
+
# Fortran module files
|
|
20
|
+
*.mod
|
|
21
|
+
*.smod
|
|
22
|
+
|
|
23
|
+
# Compiled Static libraries
|
|
24
|
+
*.lai
|
|
25
|
+
*.la
|
|
26
|
+
*.a
|
|
27
|
+
*.lib
|
|
28
|
+
|
|
29
|
+
# Executables
|
|
30
|
+
*.exe
|
|
31
|
+
*.out
|
|
32
|
+
*.app
|
|
33
|
+
|
|
34
|
+
.pio
|
|
35
|
+
.cache
|
|
36
|
+
|
|
37
|
+
compile_commands.json
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
|
|
2
|
+
This directory is intended for project header files.
|
|
3
|
+
|
|
4
|
+
A header file is a file containing C declarations and macro definitions
|
|
5
|
+
to be shared between several project source files. You request the use of a
|
|
6
|
+
header file in your project source file (C, C++, etc) located in `src` folder
|
|
7
|
+
by including it, with the C preprocessing directive `#include'.
|
|
8
|
+
|
|
9
|
+
```src/main.c
|
|
10
|
+
|
|
11
|
+
#include "header.h"
|
|
12
|
+
|
|
13
|
+
int main (void)
|
|
14
|
+
{
|
|
15
|
+
...
|
|
16
|
+
}
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Including a header file produces the same results as copying the header file
|
|
20
|
+
into each source file that needs it. Such copying would be time-consuming
|
|
21
|
+
and error-prone. With a header file, the related declarations appear
|
|
22
|
+
in only one place. If they need to be changed, they can be changed in one
|
|
23
|
+
place, and programs that include the header file will automatically use the
|
|
24
|
+
new version when next recompiled. The header file eliminates the labor of
|
|
25
|
+
finding and changing all the copies as well as the risk that a failure to
|
|
26
|
+
find one copy will result in inconsistencies within a program.
|
|
27
|
+
|
|
28
|
+
In C, the usual convention is to give header files names that end with `.h'.
|
|
29
|
+
It is most portable to use only letters, digits, dashes, and underscores in
|
|
30
|
+
header file names, and at most one dot.
|
|
31
|
+
|
|
32
|
+
Read more about using header files in official GCC documentation:
|
|
33
|
+
|
|
34
|
+
* Include Syntax
|
|
35
|
+
* Include Operation
|
|
36
|
+
* Once-Only Headers
|
|
37
|
+
* Computed Includes
|
|
38
|
+
|
|
39
|
+
https://gcc.gnu.org/onlinedocs/cpp/Header-Files.html
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
|
|
2
|
+
This directory is intended for project specific (private) libraries.
|
|
3
|
+
PlatformIO will compile them to static libraries and link into executable file.
|
|
4
|
+
|
|
5
|
+
The source code of each library should be placed in an own separate directory
|
|
6
|
+
("lib/your_library_name/[here are source files]").
|
|
7
|
+
|
|
8
|
+
For example, see a structure of the following two libraries `Foo` and `Bar`:
|
|
9
|
+
|
|
10
|
+
|--lib
|
|
11
|
+
| |
|
|
12
|
+
| |--Bar
|
|
13
|
+
| | |--docs
|
|
14
|
+
| | |--examples
|
|
15
|
+
| | |--src
|
|
16
|
+
| | |- Bar.c
|
|
17
|
+
| | |- Bar.h
|
|
18
|
+
| | |- library.json (optional, custom build options, etc) https://docs.platformio.org/page/librarymanager/config.html
|
|
19
|
+
| |
|
|
20
|
+
| |--Foo
|
|
21
|
+
| | |- Foo.c
|
|
22
|
+
| | |- Foo.h
|
|
23
|
+
| |
|
|
24
|
+
| |- README --> THIS FILE
|
|
25
|
+
|
|
|
26
|
+
|- platformio.ini
|
|
27
|
+
|--src
|
|
28
|
+
|- main.c
|
|
29
|
+
|
|
30
|
+
and a contents of `src/main.c`:
|
|
31
|
+
```
|
|
32
|
+
#include <Foo.h>
|
|
33
|
+
#include <Bar.h>
|
|
34
|
+
|
|
35
|
+
int main (void)
|
|
36
|
+
{
|
|
37
|
+
...
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
PlatformIO Library Dependency Finder will find automatically dependent
|
|
43
|
+
libraries scanning project source files.
|
|
44
|
+
|
|
45
|
+
More information about PlatformIO Library Dependency Finder
|
|
46
|
+
- https://docs.platformio.org/page/librarymanager/ldf.html
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
; PlatformIO-Konfiguration für {{projectName}}
|
|
2
|
+
;
|
|
3
|
+
; Nur ESP32-Boards. Weitere Boards über `add-board` (project-cli) oder
|
|
4
|
+
; manuell per `pio boards espressif32` suchen und [env:...]-Sektion ergänzen.
|
|
5
|
+
|
|
6
|
+
[platformio]
|
|
7
|
+
description = {{projectName}}
|
|
8
|
+
default_envs = esp32dev
|
|
9
|
+
|
|
10
|
+
[env]
|
|
11
|
+
framework = arduino
|
|
12
|
+
|
|
13
|
+
; C++17 statt PlatformIO-Default gnu++11
|
|
14
|
+
build_unflags = -std=gnu++11
|
|
15
|
+
build_flags = -std=gnu++17 -Wno-unused-variable -Wno-unused-but-set-variable -Wno-unused-function -Wno-format-extra-args
|
|
16
|
+
|
|
17
|
+
; clang-tidy für `pio check`
|
|
18
|
+
check_tool = clangtidy
|
|
19
|
+
|
|
20
|
+
; Serial-Monitor-Baudrate, muss zu Serial.begin() im Code passen
|
|
21
|
+
monitor_speed = 115200
|
|
22
|
+
|
|
23
|
+
lib_deps =
|
|
24
|
+
# Zum Einkommentieren, Beispiele:
|
|
25
|
+
# dxinteractive/ResponsiveAnalogRead ; glättet Analogwerte (Poti, Sensoren)
|
|
26
|
+
# thomasfredericks/Bounce2 ; Taster entprellen
|
|
27
|
+
# arkhipenko/TaskScheduler ; kooperatives Task-Scheduling
|
|
28
|
+
|
|
29
|
+
[env:esp32dev]
|
|
30
|
+
platform = espressif32
|
|
31
|
+
board = esp32dev
|
|
32
|
+
|
|
33
|
+
[env:featheresp32]
|
|
34
|
+
platform = espressif32
|
|
35
|
+
board = featheresp32
|
|
36
|
+
|
|
37
|
+
[env:adafruit_feather_esp32s2]
|
|
38
|
+
platform = espressif32
|
|
39
|
+
board = adafruit_feather_esp32s2
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
|
|
2
|
+
This directory is intended for PlatformIO Test Runner and project tests.
|
|
3
|
+
|
|
4
|
+
Unit Testing is a software testing method by which individual units of
|
|
5
|
+
source code, sets of one or more MCU program modules together with associated
|
|
6
|
+
control data, usage procedures, and operating procedures, are tested to
|
|
7
|
+
determine whether they are fit for use. Unit testing finds problems early
|
|
8
|
+
in the development cycle.
|
|
9
|
+
|
|
10
|
+
More information about PlatformIO Unit Testing:
|
|
11
|
+
- https://docs.platformio.org/en/latest/advanced/unit-testing/index.html
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "{{projectName}}",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"build": "tsc",
|
|
8
|
+
"dev": "node --watch src/index.ts"
|
|
9
|
+
},
|
|
10
|
+
"devDependencies": {
|
|
11
|
+
"@types/node": "^26.0.0",
|
|
12
|
+
"typescript": "^5.7.0"
|
|
13
|
+
}
|
|
14
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
console.log("{{projectName}} läuft");
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "nodenext",
|
|
5
|
+
"moduleResolution": "nodenext",
|
|
6
|
+
"types": ["node"],
|
|
7
|
+
"strict": true,
|
|
8
|
+
"outDir": "dist",
|
|
9
|
+
"rootDir": "src"
|
|
10
|
+
},
|
|
11
|
+
"include": ["src/**/*.ts"],
|
|
12
|
+
"exclude": ["node_modules", "dist"]
|
|
13
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@robineb/project-cli",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "My own Project cli tool to start projecs",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "git+https://github.com/Robin1053/Project-CLI.git"
|
|
9
|
+
},
|
|
10
|
+
"publishConfig": {
|
|
11
|
+
"access": "public"
|
|
12
|
+
},
|
|
13
|
+
"main": "./dist/index.js",
|
|
14
|
+
"scripts": {
|
|
15
|
+
"build": "tsc && node scripts/copy-templates.mjs",
|
|
16
|
+
"test": "echo \"Error: no test specified\" && exit 1"
|
|
17
|
+
},
|
|
18
|
+
"bin": {
|
|
19
|
+
"project-cli": "dist/index.js"
|
|
20
|
+
},
|
|
21
|
+
"files": [
|
|
22
|
+
"dist"
|
|
23
|
+
],
|
|
24
|
+
"type": "module",
|
|
25
|
+
"dependencies": {
|
|
26
|
+
"@clack/prompts": "^1.8.1",
|
|
27
|
+
"@napi-rs/keyring": "^2.1.0",
|
|
28
|
+
"commander": "^15.0.0",
|
|
29
|
+
"conf": "^15.1.0",
|
|
30
|
+
"execa": "^10.0.1",
|
|
31
|
+
"simple-git": "^3.36.0"
|
|
32
|
+
},
|
|
33
|
+
"devDependencies": {
|
|
34
|
+
"@types/node": "^26.6.0",
|
|
35
|
+
"tsx": "^4.23.13",
|
|
36
|
+
"typescript": "^7.0.2"
|
|
37
|
+
},
|
|
38
|
+
"allowScripts": {
|
|
39
|
+
"esbuild@0.28.2": true
|
|
40
|
+
}
|
|
41
|
+
}
|