create-innovator 0.2.0 → 0.3.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/dist/cli.js +320 -3
- package/package.json +7 -3
package/dist/cli.js
CHANGED
|
@@ -3,7 +3,306 @@
|
|
|
3
3
|
// src/cli.ts
|
|
4
4
|
import { readFileSync } from "fs";
|
|
5
5
|
import { defineCommand, runMain } from "citty";
|
|
6
|
-
import
|
|
6
|
+
import logo from "cli-ascii-logo";
|
|
7
|
+
import { intro, text, isCancel as isCancel3, outro, log } from "@clack/prompts";
|
|
8
|
+
|
|
9
|
+
// src/auth/github.ts
|
|
10
|
+
import { Octokit } from "@octokit/rest";
|
|
11
|
+
import * as p2 from "@clack/prompts";
|
|
12
|
+
|
|
13
|
+
// src/utils/constants.ts
|
|
14
|
+
var GITHUB_ORG = "stormreply";
|
|
15
|
+
var TEMPLATE_REPO = "innovator-template";
|
|
16
|
+
var REQUIRED_SCOPES = ["repo", "read:packages"];
|
|
17
|
+
var GITHUB_REGISTRY_URL = "https://npm.pkg.github.com";
|
|
18
|
+
|
|
19
|
+
// src/auth/token-storage.ts
|
|
20
|
+
import { readFile, writeFile } from "fs/promises";
|
|
21
|
+
import { homedir } from "os";
|
|
22
|
+
import { join } from "path";
|
|
23
|
+
var NPMRC_PATH = join(homedir(), ".npmrc");
|
|
24
|
+
var AUTH_TOKEN_LINE = `//npm.pkg.github.com/:_authToken=`;
|
|
25
|
+
var REGISTRY_LINE = `@${GITHUB_ORG}:registry=${GITHUB_REGISTRY_URL}`;
|
|
26
|
+
async function readNpmrc() {
|
|
27
|
+
try {
|
|
28
|
+
return await readFile(NPMRC_PATH, "utf8");
|
|
29
|
+
} catch {
|
|
30
|
+
return "";
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
async function getStoredToken() {
|
|
34
|
+
const content = await readNpmrc();
|
|
35
|
+
for (const line of content.split("\n")) {
|
|
36
|
+
const trimmed = line.trim();
|
|
37
|
+
if (trimmed.startsWith(AUTH_TOKEN_LINE)) {
|
|
38
|
+
const token = trimmed.slice(AUTH_TOKEN_LINE.length);
|
|
39
|
+
if (token) return token;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
async function saveToken(token) {
|
|
45
|
+
const content = await readNpmrc();
|
|
46
|
+
const lines = content.split("\n");
|
|
47
|
+
let hasAuthToken = false;
|
|
48
|
+
let hasRegistry = false;
|
|
49
|
+
const updated = lines.map((line) => {
|
|
50
|
+
const trimmed = line.trim();
|
|
51
|
+
if (trimmed.startsWith(AUTH_TOKEN_LINE)) {
|
|
52
|
+
hasAuthToken = true;
|
|
53
|
+
return `${AUTH_TOKEN_LINE}${token}`;
|
|
54
|
+
}
|
|
55
|
+
if (trimmed === REGISTRY_LINE) {
|
|
56
|
+
hasRegistry = true;
|
|
57
|
+
}
|
|
58
|
+
return line;
|
|
59
|
+
});
|
|
60
|
+
if (!hasAuthToken) {
|
|
61
|
+
updated.push(`${AUTH_TOKEN_LINE}${token}`);
|
|
62
|
+
}
|
|
63
|
+
if (!hasRegistry) {
|
|
64
|
+
updated.push(REGISTRY_LINE);
|
|
65
|
+
}
|
|
66
|
+
const result = updated.filter((line, i) => !(line === "" && i === updated.length - 1)).join("\n");
|
|
67
|
+
await writeFile(NPMRC_PATH, result.endsWith("\n") ? result : result + "\n", "utf8");
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// src/auth/prompts.ts
|
|
71
|
+
import * as p from "@clack/prompts";
|
|
72
|
+
async function promptForToken() {
|
|
73
|
+
p.note(
|
|
74
|
+
[
|
|
75
|
+
"A GitHub Personal Access Token (PAT) is required to access the template repository and package registry.",
|
|
76
|
+
"",
|
|
77
|
+
"1. Go to https://github.com/settings/tokens/new",
|
|
78
|
+
`2. Select scopes: ${REQUIRED_SCOPES.join(", ")}`,
|
|
79
|
+
"3. Generate and paste the token below"
|
|
80
|
+
].join("\n"),
|
|
81
|
+
"GitHub Authentication"
|
|
82
|
+
);
|
|
83
|
+
const token = await p.password({
|
|
84
|
+
message: "Enter your GitHub Personal Access Token",
|
|
85
|
+
validate(value) {
|
|
86
|
+
if (!value || value.trim().length === 0) {
|
|
87
|
+
return "Token is required";
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
if (p.isCancel(token)) {
|
|
92
|
+
p.cancel("Authentication cancelled.");
|
|
93
|
+
process.exit(0);
|
|
94
|
+
}
|
|
95
|
+
return token;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// src/auth/github.ts
|
|
99
|
+
async function validateToken(token) {
|
|
100
|
+
const octokit = new Octokit({ auth: token });
|
|
101
|
+
const response = await octokit.request("GET /user");
|
|
102
|
+
const scopeHeader = response.headers["x-oauth-scopes"] ?? "";
|
|
103
|
+
const scopes = scopeHeader.split(",").map((s) => s.trim()).filter(Boolean);
|
|
104
|
+
const missingScopes = REQUIRED_SCOPES.filter((s) => !scopes.includes(s));
|
|
105
|
+
return {
|
|
106
|
+
valid: true,
|
|
107
|
+
scopes,
|
|
108
|
+
missingScopes,
|
|
109
|
+
username: response.data.login
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
async function checkRepoAccess(token) {
|
|
113
|
+
const octokit = new Octokit({ auth: token });
|
|
114
|
+
try {
|
|
115
|
+
await octokit.rest.repos.get({ owner: GITHUB_ORG, repo: TEMPLATE_REPO });
|
|
116
|
+
return true;
|
|
117
|
+
} catch {
|
|
118
|
+
return false;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
async function checkRegistryAccess(token) {
|
|
122
|
+
const octokit = new Octokit({ auth: token });
|
|
123
|
+
try {
|
|
124
|
+
await octokit.rest.packages.listPackagesForOrganization({ org: GITHUB_ORG, package_type: "npm" });
|
|
125
|
+
return true;
|
|
126
|
+
} catch {
|
|
127
|
+
return false;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
async function ensureGitHubAuth() {
|
|
131
|
+
const s = p2.spinner();
|
|
132
|
+
s.start("Reading token from ~/.npmrc");
|
|
133
|
+
let token = await getStoredToken();
|
|
134
|
+
if (token) {
|
|
135
|
+
s.stop("Token found in ~/.npmrc");
|
|
136
|
+
} else {
|
|
137
|
+
s.stop("No token found in ~/.npmrc");
|
|
138
|
+
token = await promptForToken();
|
|
139
|
+
}
|
|
140
|
+
s.start("Validating token with GitHub");
|
|
141
|
+
const validation = await validateToken(token);
|
|
142
|
+
if (validation.missingScopes.length > 0) {
|
|
143
|
+
s.stop("Token validation failed");
|
|
144
|
+
throw new Error(
|
|
145
|
+
`Token for @${validation.username} is missing required scopes: ${validation.missingScopes.join(", ")}.
|
|
146
|
+
Please create a new token at https://github.com/settings/tokens/new with scopes: ${REQUIRED_SCOPES.join(", ")}`
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
s.stop(`Authenticated as @${validation.username}`);
|
|
150
|
+
s.start(`Checking access to ${GITHUB_ORG}/${TEMPLATE_REPO}`);
|
|
151
|
+
const hasRepoAccess = await checkRepoAccess(token);
|
|
152
|
+
if (!hasRepoAccess) {
|
|
153
|
+
s.stop("Repository access denied");
|
|
154
|
+
throw new Error(
|
|
155
|
+
`Token for @${validation.username} does not have access to ${GITHUB_ORG}/${TEMPLATE_REPO}.
|
|
156
|
+
Please ensure you are a member of the ${GITHUB_ORG} organization.`
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
s.stop(`Access to ${GITHUB_ORG}/${TEMPLATE_REPO} confirmed`);
|
|
160
|
+
s.start(`Checking access to ${GITHUB_ORG} package registry`);
|
|
161
|
+
const hasRegistryAccess = await checkRegistryAccess(token);
|
|
162
|
+
if (!hasRegistryAccess) {
|
|
163
|
+
s.stop("Registry access denied");
|
|
164
|
+
throw new Error(
|
|
165
|
+
`Token for @${validation.username} does not have access to the ${GITHUB_ORG} package registry.
|
|
166
|
+
Please ensure your token has the read:packages scope.`
|
|
167
|
+
);
|
|
168
|
+
}
|
|
169
|
+
s.stop(`Access to ${GITHUB_ORG} package registry confirmed`);
|
|
170
|
+
s.start("Saving token to ~/.npmrc");
|
|
171
|
+
await saveToken(token);
|
|
172
|
+
s.stop("Token saved to ~/.npmrc");
|
|
173
|
+
return token;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// src/scaffold/clone.ts
|
|
177
|
+
import { execFile as execFileCb } from "child_process";
|
|
178
|
+
import { promisify } from "util";
|
|
179
|
+
import { access } from "fs/promises";
|
|
180
|
+
import { Octokit as Octokit2 } from "@octokit/rest";
|
|
181
|
+
import * as p3 from "@clack/prompts";
|
|
182
|
+
var execFile = promisify(execFileCb);
|
|
183
|
+
var TAG_PREFIX_STABLE = "release-v";
|
|
184
|
+
var TAG_PREFIX_EXPERIMENTAL = "v";
|
|
185
|
+
async function ensureGhCli() {
|
|
186
|
+
try {
|
|
187
|
+
await execFile("gh", ["--version"]);
|
|
188
|
+
} catch {
|
|
189
|
+
throw new Error(
|
|
190
|
+
"GitHub CLI (gh) is not installed.\nPlease install it from https://cli.github.com and try again."
|
|
191
|
+
);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
async function fetchReleaseTags(token, includeExperimental = false) {
|
|
195
|
+
const octokit = new Octokit2({ auth: token });
|
|
196
|
+
const tags = [];
|
|
197
|
+
for await (const response of octokit.paginate.iterator(octokit.rest.repos.listTags, {
|
|
198
|
+
owner: GITHUB_ORG,
|
|
199
|
+
repo: TEMPLATE_REPO,
|
|
200
|
+
per_page: 100
|
|
201
|
+
})) {
|
|
202
|
+
for (const tag of response.data) {
|
|
203
|
+
if (tag.name.startsWith(TAG_PREFIX_STABLE)) {
|
|
204
|
+
tags.push(tag.name);
|
|
205
|
+
} else if (includeExperimental && tag.name.startsWith(TAG_PREFIX_EXPERIMENTAL)) {
|
|
206
|
+
tags.push(tag.name);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
return tags;
|
|
211
|
+
}
|
|
212
|
+
async function selectVersion(token, includeExperimental = false) {
|
|
213
|
+
const s = p3.spinner();
|
|
214
|
+
s.start("Fetching available template versions");
|
|
215
|
+
const tags = await fetchReleaseTags(token, includeExperimental);
|
|
216
|
+
s.stop(`Found ${tags.length} version(s)`);
|
|
217
|
+
if (tags.length === 0) {
|
|
218
|
+
throw new Error(`No release tags found in ${GITHUB_ORG}/${TEMPLATE_REPO}.`);
|
|
219
|
+
}
|
|
220
|
+
const selected = await p3.select({
|
|
221
|
+
message: "Select a template version",
|
|
222
|
+
options: tags.map((tag, i) => ({
|
|
223
|
+
value: tag,
|
|
224
|
+
label: tag,
|
|
225
|
+
hint: i === 0 ? "latest" : !tag.startsWith(TAG_PREFIX_STABLE) ? "experimental" : void 0
|
|
226
|
+
})),
|
|
227
|
+
initialValue: tags[0]
|
|
228
|
+
});
|
|
229
|
+
if (p3.isCancel(selected)) {
|
|
230
|
+
process.exit(0);
|
|
231
|
+
}
|
|
232
|
+
return selected;
|
|
233
|
+
}
|
|
234
|
+
async function cloneTemplate(name, tag) {
|
|
235
|
+
try {
|
|
236
|
+
await access(name);
|
|
237
|
+
throw new Error(`Directory "${name}" already exists. Please choose a different project name.`);
|
|
238
|
+
} catch (err) {
|
|
239
|
+
if (err instanceof Error && err.message.includes("already exists")) throw err;
|
|
240
|
+
}
|
|
241
|
+
const s = p3.spinner();
|
|
242
|
+
const label = tag ? `${GITHUB_ORG}/${TEMPLATE_REPO}@${tag}` : `${GITHUB_ORG}/${TEMPLATE_REPO}`;
|
|
243
|
+
s.start(`Cloning ${label}`);
|
|
244
|
+
const ghArgs = ["repo", "clone", `${GITHUB_ORG}/${TEMPLATE_REPO}`, name, "--", "--depth", "1"];
|
|
245
|
+
if (tag) ghArgs.push("--branch", tag);
|
|
246
|
+
await execFile("gh", ghArgs);
|
|
247
|
+
s.stop(`Cloned ${label}`);
|
|
248
|
+
s.start("Initializing fresh git repository");
|
|
249
|
+
await execFile("rm", ["-rf", `${name}/.git`]);
|
|
250
|
+
await execFile("git", ["init", name]);
|
|
251
|
+
s.stop("Git repository initialized");
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// src/scaffold/template.ts
|
|
255
|
+
import { readFile as readFile2, writeFile as writeFile2, readdir } from "fs/promises";
|
|
256
|
+
import { join as join2, relative } from "path";
|
|
257
|
+
import * as p4 from "@clack/prompts";
|
|
258
|
+
import { toCamelCase as toCamelCase2, toTitleCase } from "remeda";
|
|
259
|
+
|
|
260
|
+
// src/utils/case.ts
|
|
261
|
+
import { capitalize, toCamelCase } from "remeda";
|
|
262
|
+
function toPascal(kebab) {
|
|
263
|
+
return capitalize(toCamelCase(kebab));
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// src/scaffold/template.ts
|
|
267
|
+
function isBinaryBuffer(buffer) {
|
|
268
|
+
for (let i = 0; i < Math.min(buffer.length, 8e3); i++) {
|
|
269
|
+
if (buffer[i] === 0) return true;
|
|
270
|
+
}
|
|
271
|
+
return false;
|
|
272
|
+
}
|
|
273
|
+
async function replaceTemplateNames(dir, projectName) {
|
|
274
|
+
const s = p4.spinner();
|
|
275
|
+
s.start("Replacing template names");
|
|
276
|
+
const replacements = /* @__PURE__ */ new Map([
|
|
277
|
+
[TEMPLATE_REPO, projectName],
|
|
278
|
+
[toCamelCase2(TEMPLATE_REPO), toCamelCase2(projectName)],
|
|
279
|
+
[toPascal(TEMPLATE_REPO), toPascal(projectName)],
|
|
280
|
+
[toTitleCase(TEMPLATE_REPO), toTitleCase(projectName)]
|
|
281
|
+
]);
|
|
282
|
+
const allFiles = await readdir(dir, { recursive: true, withFileTypes: true });
|
|
283
|
+
const files = allFiles.filter((f) => f.isFile()).map((f) => relative(dir, join2(f.parentPath, f.name)));
|
|
284
|
+
let replacedCount = 0;
|
|
285
|
+
for (const file of files) {
|
|
286
|
+
const filePath = join2(dir, file);
|
|
287
|
+
const buffer = await readFile2(filePath);
|
|
288
|
+
if (isBinaryBuffer(buffer)) continue;
|
|
289
|
+
let content = buffer.toString("utf8");
|
|
290
|
+
let changed = false;
|
|
291
|
+
for (const [from, to] of replacements) {
|
|
292
|
+
if (content.includes(from)) {
|
|
293
|
+
content = content.replaceAll(from, to);
|
|
294
|
+
changed = true;
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
if (changed) {
|
|
298
|
+
await writeFile2(filePath, content, "utf8");
|
|
299
|
+
replacedCount++;
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
s.stop(`Replaced template names in ${replacedCount} file(s)`);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// src/cli.ts
|
|
7
306
|
var pkgUrl = new URL("../package.json", import.meta.url);
|
|
8
307
|
var pkg = JSON.parse(readFileSync(pkgUrl, "utf8"));
|
|
9
308
|
var version = pkg.version ?? "0.0.0";
|
|
@@ -19,19 +318,37 @@ var main = defineCommand({
|
|
|
19
318
|
description: "Project name",
|
|
20
319
|
required: false,
|
|
21
320
|
type: "string"
|
|
321
|
+
},
|
|
322
|
+
experimental: {
|
|
323
|
+
alias: ["e"],
|
|
324
|
+
description: "Include experimental (pre-release) versions",
|
|
325
|
+
required: false,
|
|
326
|
+
type: "boolean",
|
|
327
|
+
default: false
|
|
22
328
|
}
|
|
23
329
|
},
|
|
24
330
|
async run({ args }) {
|
|
331
|
+
console.log(logo.createLogo("Innovator", "aurora"));
|
|
25
332
|
intro(`Create Innovator App (v${version})`);
|
|
26
333
|
const projectName = args.name ?? await text({
|
|
27
334
|
defaultValue: "my-innovator-app",
|
|
28
335
|
message: "Project name",
|
|
29
336
|
placeholder: "my-innovator-app"
|
|
30
337
|
});
|
|
31
|
-
if (
|
|
338
|
+
if (isCancel3(projectName)) {
|
|
32
339
|
process.exit(0);
|
|
33
340
|
}
|
|
34
|
-
|
|
341
|
+
try {
|
|
342
|
+
const token = await ensureGitHubAuth();
|
|
343
|
+
await ensureGhCli();
|
|
344
|
+
const tag = await selectVersion(token, args.experimental);
|
|
345
|
+
await cloneTemplate(projectName, tag);
|
|
346
|
+
await replaceTemplateNames(projectName, projectName);
|
|
347
|
+
} catch (error) {
|
|
348
|
+
log.error(error instanceof Error ? error.message : "Scaffolding failed.");
|
|
349
|
+
process.exit(1);
|
|
350
|
+
}
|
|
351
|
+
outro(`Project ${projectName} is ready!`);
|
|
35
352
|
}
|
|
36
353
|
});
|
|
37
354
|
runMain(main);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-innovator",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Create Innovator App",
|
|
5
5
|
"author": {
|
|
6
6
|
"email": "storm@reply.de",
|
|
@@ -24,8 +24,11 @@
|
|
|
24
24
|
"pnpm": ">=10.29.2"
|
|
25
25
|
},
|
|
26
26
|
"dependencies": {
|
|
27
|
-
"@clack/prompts": "1.0.
|
|
28
|
-
"
|
|
27
|
+
"@clack/prompts": "1.0.1",
|
|
28
|
+
"@octokit/rest": "22.0.1",
|
|
29
|
+
"citty": "0.2.1",
|
|
30
|
+
"cli-ascii-logo": "2.1.0",
|
|
31
|
+
"remeda": "2.33.6"
|
|
29
32
|
},
|
|
30
33
|
"devDependencies": {
|
|
31
34
|
"@commitlint/cli": "20.4.1",
|
|
@@ -40,6 +43,7 @@
|
|
|
40
43
|
"globals": "17.3.0",
|
|
41
44
|
"husky": "9.1.7",
|
|
42
45
|
"lint-staged": "16.2.7",
|
|
46
|
+
"memfs": "4.56.10",
|
|
43
47
|
"prettier": "3.8.1",
|
|
44
48
|
"release-it": "19.2.4",
|
|
45
49
|
"release-it-pnpm": "4.6.6",
|