gistajs 0.0.0 → 0.0.2
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/README.md +25 -0
- package/dist/bin.cjs +360 -0
- package/dist/index.d.ts +20 -0
- package/dist/index.js +336 -0
- package/package.json +55 -1
- package/.gitattributes +0 -2
package/README.md
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# gistajs
|
|
2
|
+
|
|
3
|
+
Small CLI for creating Gista.js starter projects.
|
|
4
|
+
|
|
5
|
+
## Usage
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npx gistajs create my-app
|
|
9
|
+
npx gistajs create my-app --starter website
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
## Development
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
pnpm install
|
|
16
|
+
pnpm typecheck
|
|
17
|
+
pnpm build
|
|
18
|
+
pnpm test -- --run
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Release
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
pnpm np
|
|
25
|
+
```
|
package/dist/bin.cjs
ADDED
|
@@ -0,0 +1,360 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
var __create = Object.create;
|
|
4
|
+
var __defProp = Object.defineProperty;
|
|
5
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
6
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
7
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
8
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
9
|
+
var __copyProps = (to, from, except, desc) => {
|
|
10
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
11
|
+
for (let key of __getOwnPropNames(from))
|
|
12
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
13
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
14
|
+
}
|
|
15
|
+
return to;
|
|
16
|
+
};
|
|
17
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
18
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
19
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
20
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
21
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
22
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
23
|
+
mod
|
|
24
|
+
));
|
|
25
|
+
|
|
26
|
+
// src/cli.ts
|
|
27
|
+
var import_node_process3 = __toESM(require("process"), 1);
|
|
28
|
+
|
|
29
|
+
// src/catalog.ts
|
|
30
|
+
var DEFAULT_CATALOG_URL = "https://gistajs.com/manifests/starters.json";
|
|
31
|
+
async function loadCatalog(url = DEFAULT_CATALOG_URL) {
|
|
32
|
+
let response = await fetch(url);
|
|
33
|
+
if (!response.ok) {
|
|
34
|
+
throw new Error(`Catalog request failed: ${response.status}`);
|
|
35
|
+
}
|
|
36
|
+
let data = await response.json();
|
|
37
|
+
return parseCatalog(data);
|
|
38
|
+
}
|
|
39
|
+
function parseCatalog(data) {
|
|
40
|
+
if (!Array.isArray(data)) throw new Error("Invalid starter catalog");
|
|
41
|
+
return data.map(parseStarter);
|
|
42
|
+
}
|
|
43
|
+
function parseStarter(data) {
|
|
44
|
+
if (!data || typeof data !== "object") throw new Error("Invalid starter");
|
|
45
|
+
let entry = data;
|
|
46
|
+
let slug = entry.slug;
|
|
47
|
+
let repo = entry.repo;
|
|
48
|
+
let branches = entry.branches;
|
|
49
|
+
let description = entry.description;
|
|
50
|
+
if (typeof slug !== "string" || typeof repo !== "string" || typeof description !== "string") {
|
|
51
|
+
throw new Error("Invalid starter entry");
|
|
52
|
+
}
|
|
53
|
+
if (!Array.isArray(branches) || branches.length === 0 || branches.some((branch) => typeof branch !== "string" || !branch)) {
|
|
54
|
+
throw new Error(`Invalid branches for ${slug}`);
|
|
55
|
+
}
|
|
56
|
+
return { slug, repo, branches, description };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// src/create.ts
|
|
60
|
+
var import_promises2 = require("fs/promises");
|
|
61
|
+
var import_node_os = require("os");
|
|
62
|
+
var import_node_path = require("path");
|
|
63
|
+
var import_node_process2 = __toESM(require("process"), 1);
|
|
64
|
+
var tar = __toESM(require("tar"), 1);
|
|
65
|
+
|
|
66
|
+
// src/git.ts
|
|
67
|
+
var import_node_child_process = require("child_process");
|
|
68
|
+
|
|
69
|
+
// src/prompt.ts
|
|
70
|
+
var import_node_process = __toESM(require("process"), 1);
|
|
71
|
+
var import_promises = __toESM(require("readline/promises"), 1);
|
|
72
|
+
async function promptForStarter(starters) {
|
|
73
|
+
let rl = createPrompt();
|
|
74
|
+
try {
|
|
75
|
+
let options = starters.map(
|
|
76
|
+
(starter2, index2) => `${index2 + 1}. ${starter2.slug} - ${starter2.description}`
|
|
77
|
+
).join("\n");
|
|
78
|
+
let answer = await rl.question(`Choose a starter:
|
|
79
|
+
${options}
|
|
80
|
+
> `);
|
|
81
|
+
let index = Number.parseInt(answer.trim(), 10) - 1;
|
|
82
|
+
let starter = starters[index];
|
|
83
|
+
if (!starter) throw new Error("Invalid starter selection");
|
|
84
|
+
return starter.slug;
|
|
85
|
+
} finally {
|
|
86
|
+
rl.close();
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
async function promptForGitIdentity() {
|
|
90
|
+
let rl = createPrompt();
|
|
91
|
+
try {
|
|
92
|
+
console.log(
|
|
93
|
+
"Before I make the first commit, Git needs a name and email to attach to it."
|
|
94
|
+
);
|
|
95
|
+
let name = (await rl.question("Your name: ")).trim();
|
|
96
|
+
let email = (await rl.question("Your email: ")).trim();
|
|
97
|
+
if (!name) throw new Error("Name is required to make the first commit");
|
|
98
|
+
if (!email) throw new Error("Email is required to make the first commit");
|
|
99
|
+
let saveGlobal = await confirm(
|
|
100
|
+
rl,
|
|
101
|
+
"Save this as your default Git identity for future projects too? (y/N) "
|
|
102
|
+
);
|
|
103
|
+
return { name, email, saveGlobal };
|
|
104
|
+
} finally {
|
|
105
|
+
rl.close();
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
function createPrompt() {
|
|
109
|
+
return import_promises.default.createInterface({
|
|
110
|
+
input: import_node_process.default.stdin,
|
|
111
|
+
output: import_node_process.default.stdout
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
async function confirm(rl, message) {
|
|
115
|
+
let answer = (await rl.question(message)).trim().toLowerCase();
|
|
116
|
+
return answer === "y" || answer === "yes";
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// src/git.ts
|
|
120
|
+
var defaultDeps = {
|
|
121
|
+
promptForGitIdentity,
|
|
122
|
+
readGitConfig,
|
|
123
|
+
run
|
|
124
|
+
};
|
|
125
|
+
async function initGit(root, starter, deps = defaultDeps) {
|
|
126
|
+
await git(root, ["init", "-q", "-b", "main"], deps);
|
|
127
|
+
let identity = await resolveGitIdentity(root, deps);
|
|
128
|
+
if (!identity) {
|
|
129
|
+
identity = await deps.promptForGitIdentity();
|
|
130
|
+
await git(root, ["config", "user.name", identity.name], deps);
|
|
131
|
+
await git(root, ["config", "user.email", identity.email], deps);
|
|
132
|
+
if (identity.saveGlobal) {
|
|
133
|
+
await git(root, ["config", "--global", "user.name", identity.name], deps);
|
|
134
|
+
await git(
|
|
135
|
+
root,
|
|
136
|
+
["config", "--global", "user.email", identity.email],
|
|
137
|
+
deps
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
await git(root, ["add", "."], deps);
|
|
142
|
+
await git(
|
|
143
|
+
root,
|
|
144
|
+
["commit", "--quiet", "-m", "Initial commit from gistajs"],
|
|
145
|
+
deps
|
|
146
|
+
);
|
|
147
|
+
for (let branch of starter.branches.slice(1)) {
|
|
148
|
+
await git(root, ["branch", branch], deps);
|
|
149
|
+
}
|
|
150
|
+
let defaultBranch = starter.branches[0];
|
|
151
|
+
if (defaultBranch && defaultBranch !== "main") {
|
|
152
|
+
await git(root, ["checkout", "-q", defaultBranch], deps);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
async function git(cwd, args, deps) {
|
|
156
|
+
await deps.run("git", args, cwd);
|
|
157
|
+
}
|
|
158
|
+
async function resolveGitIdentity(cwd, deps) {
|
|
159
|
+
let name = deps.readGitConfig(cwd, "user.name");
|
|
160
|
+
let email = deps.readGitConfig(cwd, "user.email");
|
|
161
|
+
if (!name || !email) return null;
|
|
162
|
+
return { name, email, saveGlobal: false };
|
|
163
|
+
}
|
|
164
|
+
function readGitConfig(cwd, key) {
|
|
165
|
+
try {
|
|
166
|
+
return (0, import_node_child_process.execFileSync)("git", ["config", "--get", key], {
|
|
167
|
+
cwd,
|
|
168
|
+
encoding: "utf-8"
|
|
169
|
+
}).trim();
|
|
170
|
+
} catch {
|
|
171
|
+
return "";
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
async function run(command, args, cwd) {
|
|
175
|
+
await new Promise((resolve2, reject) => {
|
|
176
|
+
let child = (0, import_node_child_process.spawn)(command, args, {
|
|
177
|
+
cwd,
|
|
178
|
+
stdio: "inherit"
|
|
179
|
+
});
|
|
180
|
+
child.once("error", reject);
|
|
181
|
+
child.once("exit", (code) => {
|
|
182
|
+
if (code === 0) resolve2();
|
|
183
|
+
else
|
|
184
|
+
reject(
|
|
185
|
+
new Error(`${command} ${args.join(" ")} exited with code ${code}`)
|
|
186
|
+
);
|
|
187
|
+
});
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// src/create.ts
|
|
192
|
+
async function createProject(starter, options) {
|
|
193
|
+
let root = (0, import_node_path.resolve)(
|
|
194
|
+
import_node_process2.default.cwd(),
|
|
195
|
+
options.targetDir || options.projectName || starter.slug
|
|
196
|
+
);
|
|
197
|
+
await assertEmptyTarget(root);
|
|
198
|
+
let staging = await (0, import_promises2.mkdtemp)((0, import_node_path.join)((0, import_node_os.tmpdir)(), "gistajs-"));
|
|
199
|
+
let archivePath = (0, import_node_path.join)(staging, "starter.tgz");
|
|
200
|
+
let extractRoot = (0, import_node_path.join)(staging, "extract");
|
|
201
|
+
try {
|
|
202
|
+
await (0, import_promises2.mkdir)(extractRoot, { recursive: true });
|
|
203
|
+
await downloadStarter(starter, archivePath);
|
|
204
|
+
await extractStarter(archivePath, extractRoot);
|
|
205
|
+
let extracted = await findExtractedRoot(extractRoot);
|
|
206
|
+
await assertSafeProjectRoot(root);
|
|
207
|
+
await (0, import_promises2.rename)(extracted, root);
|
|
208
|
+
await rewritePackageName(root, (0, import_node_path.basename)(root));
|
|
209
|
+
if (options.git !== false) {
|
|
210
|
+
await initGit(root, starter);
|
|
211
|
+
}
|
|
212
|
+
if (options.install !== false) {
|
|
213
|
+
await run("pnpm", ["install"], root);
|
|
214
|
+
}
|
|
215
|
+
return root;
|
|
216
|
+
} finally {
|
|
217
|
+
await (0, import_promises2.rm)(staging, { recursive: true, force: true });
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
function getStarterTarballUrl(starter) {
|
|
221
|
+
let defaultBranch = starter.branches[0];
|
|
222
|
+
if (!defaultBranch) {
|
|
223
|
+
throw new Error(`Starter ${starter.slug} has no branches configured`);
|
|
224
|
+
}
|
|
225
|
+
return `https://codeload.github.com/${starter.repo}/tar.gz/refs/heads/${defaultBranch}`;
|
|
226
|
+
}
|
|
227
|
+
async function downloadStarter(starter, destination) {
|
|
228
|
+
let response = await fetch(getStarterTarballUrl(starter));
|
|
229
|
+
if (!response.ok || !response.body) {
|
|
230
|
+
throw new Error(`Failed to download ${starter.slug} starter`);
|
|
231
|
+
}
|
|
232
|
+
let chunks = [];
|
|
233
|
+
for await (let chunk of response.body) {
|
|
234
|
+
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
235
|
+
}
|
|
236
|
+
await (0, import_promises2.writeFile)(destination, Buffer.concat(chunks));
|
|
237
|
+
}
|
|
238
|
+
async function extractStarter(archivePath, destination) {
|
|
239
|
+
await tar.x({
|
|
240
|
+
file: archivePath,
|
|
241
|
+
cwd: destination,
|
|
242
|
+
strict: true,
|
|
243
|
+
onentry: (entry) => {
|
|
244
|
+
let normalized = entry.path.replace(/\\/g, "/");
|
|
245
|
+
if (normalized.startsWith("/") || normalized.includes("../")) {
|
|
246
|
+
throw new Error(`Unsafe archive entry: ${entry.path}`);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
async function findExtractedRoot(root) {
|
|
252
|
+
let children = await (0, import_promises2.readdir)(root, { withFileTypes: true });
|
|
253
|
+
let directories = children.filter((entry) => entry.isDirectory());
|
|
254
|
+
if (directories.length !== 1) {
|
|
255
|
+
throw new Error("Expected one extracted starter directory");
|
|
256
|
+
}
|
|
257
|
+
return (0, import_node_path.join)(root, directories[0].name);
|
|
258
|
+
}
|
|
259
|
+
async function rewritePackageName(root, projectName) {
|
|
260
|
+
let path = (0, import_node_path.join)(root, "package.json");
|
|
261
|
+
let source = await (0, import_promises2.readFile)(path, "utf8");
|
|
262
|
+
let pkg = JSON.parse(source);
|
|
263
|
+
if (typeof pkg.name === "string") {
|
|
264
|
+
pkg.name = projectName;
|
|
265
|
+
await (0, import_promises2.writeFile)(path, `${JSON.stringify(pkg, null, 2)}
|
|
266
|
+
`);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
async function assertEmptyTarget(root) {
|
|
270
|
+
try {
|
|
271
|
+
await (0, import_promises2.stat)(root);
|
|
272
|
+
throw new Error(`Target path already exists: ${root}`);
|
|
273
|
+
} catch (error) {
|
|
274
|
+
if (error.code !== "ENOENT") throw error;
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
async function assertSafeProjectRoot(root) {
|
|
278
|
+
let parent = (0, import_node_path.dirname)(root);
|
|
279
|
+
await (0, import_promises2.mkdir)(parent, { recursive: true });
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// src/cli.ts
|
|
283
|
+
async function runCli(argv = import_node_process3.default.argv.slice(2)) {
|
|
284
|
+
let [command, ...rest] = argv;
|
|
285
|
+
if (!command || command === "--help" || command === "-h") {
|
|
286
|
+
printHelp();
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
if (command !== "create") {
|
|
290
|
+
throw new Error(`Unknown command: ${command}`);
|
|
291
|
+
}
|
|
292
|
+
let options = parseCreateArgs(rest);
|
|
293
|
+
let catalog = await loadCatalog(options.catalogUrl);
|
|
294
|
+
let starterName = options.starter || await promptForStarter(catalog);
|
|
295
|
+
let starter = catalog.find((entry) => entry.slug === starterName);
|
|
296
|
+
if (!starter) {
|
|
297
|
+
throw new Error(`Unknown starter: ${starterName}`);
|
|
298
|
+
}
|
|
299
|
+
if (!options.projectName) {
|
|
300
|
+
throw new Error("Project name is required");
|
|
301
|
+
}
|
|
302
|
+
let root = await createProject(starter, options);
|
|
303
|
+
console.log(`Created ${starter.slug} project in ${root}`);
|
|
304
|
+
}
|
|
305
|
+
async function main() {
|
|
306
|
+
try {
|
|
307
|
+
await runCli();
|
|
308
|
+
} catch (error) {
|
|
309
|
+
let message = error instanceof Error ? error.message : String(error);
|
|
310
|
+
console.error(message);
|
|
311
|
+
import_node_process3.default.exitCode = 1;
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
function parseCreateArgs(argv) {
|
|
315
|
+
let options = {
|
|
316
|
+
install: true,
|
|
317
|
+
git: true
|
|
318
|
+
};
|
|
319
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
320
|
+
let arg = argv[index];
|
|
321
|
+
if (!arg) continue;
|
|
322
|
+
if (!arg.startsWith("--") && !options.projectName) {
|
|
323
|
+
options.projectName = arg;
|
|
324
|
+
continue;
|
|
325
|
+
}
|
|
326
|
+
if (arg === "--starter") {
|
|
327
|
+
if (!argv[index + 1]) throw new Error("--starter requires a value");
|
|
328
|
+
options.starter = argv[index + 1];
|
|
329
|
+
index += 1;
|
|
330
|
+
continue;
|
|
331
|
+
}
|
|
332
|
+
if (arg === "--no-install") {
|
|
333
|
+
options.install = false;
|
|
334
|
+
continue;
|
|
335
|
+
}
|
|
336
|
+
if (arg === "--no-git") {
|
|
337
|
+
options.git = false;
|
|
338
|
+
continue;
|
|
339
|
+
}
|
|
340
|
+
if (arg === "--catalog-url") {
|
|
341
|
+
if (!argv[index + 1]) throw new Error("--catalog-url requires a value");
|
|
342
|
+
options.catalogUrl = argv[index + 1];
|
|
343
|
+
index += 1;
|
|
344
|
+
continue;
|
|
345
|
+
}
|
|
346
|
+
throw new Error(`Unknown argument: ${arg}`);
|
|
347
|
+
}
|
|
348
|
+
return options;
|
|
349
|
+
}
|
|
350
|
+
function printHelp() {
|
|
351
|
+
console.log(
|
|
352
|
+
[
|
|
353
|
+
"Usage:",
|
|
354
|
+
" gistajs create <project-name> [--starter <slug>] [--no-install] [--no-git]"
|
|
355
|
+
].join("\n")
|
|
356
|
+
);
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
// src/bin.ts
|
|
360
|
+
void main();
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
type StarterSpec = {
|
|
2
|
+
slug: string;
|
|
3
|
+
repo: string;
|
|
4
|
+
branches: string[];
|
|
5
|
+
description: string;
|
|
6
|
+
};
|
|
7
|
+
type CreateOptions = {
|
|
8
|
+
projectName?: string;
|
|
9
|
+
starter?: string;
|
|
10
|
+
targetDir?: string;
|
|
11
|
+
install?: boolean;
|
|
12
|
+
git?: boolean;
|
|
13
|
+
catalogUrl?: string;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
declare function runCli(argv?: string[]): Promise<void>;
|
|
17
|
+
|
|
18
|
+
declare function createProject(starter: StarterSpec, options: CreateOptions): Promise<string>;
|
|
19
|
+
|
|
20
|
+
export { type CreateOptions, type StarterSpec, createProject, runCli };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,336 @@
|
|
|
1
|
+
// src/cli.ts
|
|
2
|
+
import process3 from "process";
|
|
3
|
+
|
|
4
|
+
// src/catalog.ts
|
|
5
|
+
var DEFAULT_CATALOG_URL = "https://gistajs.com/manifests/starters.json";
|
|
6
|
+
async function loadCatalog(url = DEFAULT_CATALOG_URL) {
|
|
7
|
+
let response = await fetch(url);
|
|
8
|
+
if (!response.ok) {
|
|
9
|
+
throw new Error(`Catalog request failed: ${response.status}`);
|
|
10
|
+
}
|
|
11
|
+
let data = await response.json();
|
|
12
|
+
return parseCatalog(data);
|
|
13
|
+
}
|
|
14
|
+
function parseCatalog(data) {
|
|
15
|
+
if (!Array.isArray(data)) throw new Error("Invalid starter catalog");
|
|
16
|
+
return data.map(parseStarter);
|
|
17
|
+
}
|
|
18
|
+
function parseStarter(data) {
|
|
19
|
+
if (!data || typeof data !== "object") throw new Error("Invalid starter");
|
|
20
|
+
let entry = data;
|
|
21
|
+
let slug = entry.slug;
|
|
22
|
+
let repo = entry.repo;
|
|
23
|
+
let branches = entry.branches;
|
|
24
|
+
let description = entry.description;
|
|
25
|
+
if (typeof slug !== "string" || typeof repo !== "string" || typeof description !== "string") {
|
|
26
|
+
throw new Error("Invalid starter entry");
|
|
27
|
+
}
|
|
28
|
+
if (!Array.isArray(branches) || branches.length === 0 || branches.some((branch) => typeof branch !== "string" || !branch)) {
|
|
29
|
+
throw new Error(`Invalid branches for ${slug}`);
|
|
30
|
+
}
|
|
31
|
+
return { slug, repo, branches, description };
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// src/create.ts
|
|
35
|
+
import {
|
|
36
|
+
mkdtemp,
|
|
37
|
+
mkdir,
|
|
38
|
+
readdir,
|
|
39
|
+
readFile,
|
|
40
|
+
rename,
|
|
41
|
+
rm,
|
|
42
|
+
stat,
|
|
43
|
+
writeFile
|
|
44
|
+
} from "fs/promises";
|
|
45
|
+
import { tmpdir } from "os";
|
|
46
|
+
import { basename, dirname, join, resolve } from "path";
|
|
47
|
+
import process2 from "process";
|
|
48
|
+
import * as tar from "tar";
|
|
49
|
+
|
|
50
|
+
// src/git.ts
|
|
51
|
+
import { execFileSync, spawn } from "child_process";
|
|
52
|
+
|
|
53
|
+
// src/prompt.ts
|
|
54
|
+
import process from "process";
|
|
55
|
+
import readline from "readline/promises";
|
|
56
|
+
async function promptForStarter(starters) {
|
|
57
|
+
let rl = createPrompt();
|
|
58
|
+
try {
|
|
59
|
+
let options = starters.map(
|
|
60
|
+
(starter2, index2) => `${index2 + 1}. ${starter2.slug} - ${starter2.description}`
|
|
61
|
+
).join("\n");
|
|
62
|
+
let answer = await rl.question(`Choose a starter:
|
|
63
|
+
${options}
|
|
64
|
+
> `);
|
|
65
|
+
let index = Number.parseInt(answer.trim(), 10) - 1;
|
|
66
|
+
let starter = starters[index];
|
|
67
|
+
if (!starter) throw new Error("Invalid starter selection");
|
|
68
|
+
return starter.slug;
|
|
69
|
+
} finally {
|
|
70
|
+
rl.close();
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
async function promptForGitIdentity() {
|
|
74
|
+
let rl = createPrompt();
|
|
75
|
+
try {
|
|
76
|
+
console.log(
|
|
77
|
+
"Before I make the first commit, Git needs a name and email to attach to it."
|
|
78
|
+
);
|
|
79
|
+
let name = (await rl.question("Your name: ")).trim();
|
|
80
|
+
let email = (await rl.question("Your email: ")).trim();
|
|
81
|
+
if (!name) throw new Error("Name is required to make the first commit");
|
|
82
|
+
if (!email) throw new Error("Email is required to make the first commit");
|
|
83
|
+
let saveGlobal = await confirm(
|
|
84
|
+
rl,
|
|
85
|
+
"Save this as your default Git identity for future projects too? (y/N) "
|
|
86
|
+
);
|
|
87
|
+
return { name, email, saveGlobal };
|
|
88
|
+
} finally {
|
|
89
|
+
rl.close();
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
function createPrompt() {
|
|
93
|
+
return readline.createInterface({
|
|
94
|
+
input: process.stdin,
|
|
95
|
+
output: process.stdout
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
async function confirm(rl, message) {
|
|
99
|
+
let answer = (await rl.question(message)).trim().toLowerCase();
|
|
100
|
+
return answer === "y" || answer === "yes";
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// src/git.ts
|
|
104
|
+
var defaultDeps = {
|
|
105
|
+
promptForGitIdentity,
|
|
106
|
+
readGitConfig,
|
|
107
|
+
run
|
|
108
|
+
};
|
|
109
|
+
async function initGit(root, starter, deps = defaultDeps) {
|
|
110
|
+
await git(root, ["init", "-q", "-b", "main"], deps);
|
|
111
|
+
let identity = await resolveGitIdentity(root, deps);
|
|
112
|
+
if (!identity) {
|
|
113
|
+
identity = await deps.promptForGitIdentity();
|
|
114
|
+
await git(root, ["config", "user.name", identity.name], deps);
|
|
115
|
+
await git(root, ["config", "user.email", identity.email], deps);
|
|
116
|
+
if (identity.saveGlobal) {
|
|
117
|
+
await git(root, ["config", "--global", "user.name", identity.name], deps);
|
|
118
|
+
await git(
|
|
119
|
+
root,
|
|
120
|
+
["config", "--global", "user.email", identity.email],
|
|
121
|
+
deps
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
await git(root, ["add", "."], deps);
|
|
126
|
+
await git(
|
|
127
|
+
root,
|
|
128
|
+
["commit", "--quiet", "-m", "Initial commit from gistajs"],
|
|
129
|
+
deps
|
|
130
|
+
);
|
|
131
|
+
for (let branch of starter.branches.slice(1)) {
|
|
132
|
+
await git(root, ["branch", branch], deps);
|
|
133
|
+
}
|
|
134
|
+
let defaultBranch = starter.branches[0];
|
|
135
|
+
if (defaultBranch && defaultBranch !== "main") {
|
|
136
|
+
await git(root, ["checkout", "-q", defaultBranch], deps);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
async function git(cwd, args, deps) {
|
|
140
|
+
await deps.run("git", args, cwd);
|
|
141
|
+
}
|
|
142
|
+
async function resolveGitIdentity(cwd, deps) {
|
|
143
|
+
let name = deps.readGitConfig(cwd, "user.name");
|
|
144
|
+
let email = deps.readGitConfig(cwd, "user.email");
|
|
145
|
+
if (!name || !email) return null;
|
|
146
|
+
return { name, email, saveGlobal: false };
|
|
147
|
+
}
|
|
148
|
+
function readGitConfig(cwd, key) {
|
|
149
|
+
try {
|
|
150
|
+
return execFileSync("git", ["config", "--get", key], {
|
|
151
|
+
cwd,
|
|
152
|
+
encoding: "utf-8"
|
|
153
|
+
}).trim();
|
|
154
|
+
} catch {
|
|
155
|
+
return "";
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
async function run(command, args, cwd) {
|
|
159
|
+
await new Promise((resolve2, reject) => {
|
|
160
|
+
let child = spawn(command, args, {
|
|
161
|
+
cwd,
|
|
162
|
+
stdio: "inherit"
|
|
163
|
+
});
|
|
164
|
+
child.once("error", reject);
|
|
165
|
+
child.once("exit", (code) => {
|
|
166
|
+
if (code === 0) resolve2();
|
|
167
|
+
else
|
|
168
|
+
reject(
|
|
169
|
+
new Error(`${command} ${args.join(" ")} exited with code ${code}`)
|
|
170
|
+
);
|
|
171
|
+
});
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// src/create.ts
|
|
176
|
+
async function createProject(starter, options) {
|
|
177
|
+
let root = resolve(
|
|
178
|
+
process2.cwd(),
|
|
179
|
+
options.targetDir || options.projectName || starter.slug
|
|
180
|
+
);
|
|
181
|
+
await assertEmptyTarget(root);
|
|
182
|
+
let staging = await mkdtemp(join(tmpdir(), "gistajs-"));
|
|
183
|
+
let archivePath = join(staging, "starter.tgz");
|
|
184
|
+
let extractRoot = join(staging, "extract");
|
|
185
|
+
try {
|
|
186
|
+
await mkdir(extractRoot, { recursive: true });
|
|
187
|
+
await downloadStarter(starter, archivePath);
|
|
188
|
+
await extractStarter(archivePath, extractRoot);
|
|
189
|
+
let extracted = await findExtractedRoot(extractRoot);
|
|
190
|
+
await assertSafeProjectRoot(root);
|
|
191
|
+
await rename(extracted, root);
|
|
192
|
+
await rewritePackageName(root, basename(root));
|
|
193
|
+
if (options.git !== false) {
|
|
194
|
+
await initGit(root, starter);
|
|
195
|
+
}
|
|
196
|
+
if (options.install !== false) {
|
|
197
|
+
await run("pnpm", ["install"], root);
|
|
198
|
+
}
|
|
199
|
+
return root;
|
|
200
|
+
} finally {
|
|
201
|
+
await rm(staging, { recursive: true, force: true });
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
function getStarterTarballUrl(starter) {
|
|
205
|
+
let defaultBranch = starter.branches[0];
|
|
206
|
+
if (!defaultBranch) {
|
|
207
|
+
throw new Error(`Starter ${starter.slug} has no branches configured`);
|
|
208
|
+
}
|
|
209
|
+
return `https://codeload.github.com/${starter.repo}/tar.gz/refs/heads/${defaultBranch}`;
|
|
210
|
+
}
|
|
211
|
+
async function downloadStarter(starter, destination) {
|
|
212
|
+
let response = await fetch(getStarterTarballUrl(starter));
|
|
213
|
+
if (!response.ok || !response.body) {
|
|
214
|
+
throw new Error(`Failed to download ${starter.slug} starter`);
|
|
215
|
+
}
|
|
216
|
+
let chunks = [];
|
|
217
|
+
for await (let chunk of response.body) {
|
|
218
|
+
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
219
|
+
}
|
|
220
|
+
await writeFile(destination, Buffer.concat(chunks));
|
|
221
|
+
}
|
|
222
|
+
async function extractStarter(archivePath, destination) {
|
|
223
|
+
await tar.x({
|
|
224
|
+
file: archivePath,
|
|
225
|
+
cwd: destination,
|
|
226
|
+
strict: true,
|
|
227
|
+
onentry: (entry) => {
|
|
228
|
+
let normalized = entry.path.replace(/\\/g, "/");
|
|
229
|
+
if (normalized.startsWith("/") || normalized.includes("../")) {
|
|
230
|
+
throw new Error(`Unsafe archive entry: ${entry.path}`);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
async function findExtractedRoot(root) {
|
|
236
|
+
let children = await readdir(root, { withFileTypes: true });
|
|
237
|
+
let directories = children.filter((entry) => entry.isDirectory());
|
|
238
|
+
if (directories.length !== 1) {
|
|
239
|
+
throw new Error("Expected one extracted starter directory");
|
|
240
|
+
}
|
|
241
|
+
return join(root, directories[0].name);
|
|
242
|
+
}
|
|
243
|
+
async function rewritePackageName(root, projectName) {
|
|
244
|
+
let path = join(root, "package.json");
|
|
245
|
+
let source = await readFile(path, "utf8");
|
|
246
|
+
let pkg = JSON.parse(source);
|
|
247
|
+
if (typeof pkg.name === "string") {
|
|
248
|
+
pkg.name = projectName;
|
|
249
|
+
await writeFile(path, `${JSON.stringify(pkg, null, 2)}
|
|
250
|
+
`);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
async function assertEmptyTarget(root) {
|
|
254
|
+
try {
|
|
255
|
+
await stat(root);
|
|
256
|
+
throw new Error(`Target path already exists: ${root}`);
|
|
257
|
+
} catch (error) {
|
|
258
|
+
if (error.code !== "ENOENT") throw error;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
async function assertSafeProjectRoot(root) {
|
|
262
|
+
let parent = dirname(root);
|
|
263
|
+
await mkdir(parent, { recursive: true });
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// src/cli.ts
|
|
267
|
+
async function runCli(argv = process3.argv.slice(2)) {
|
|
268
|
+
let [command, ...rest] = argv;
|
|
269
|
+
if (!command || command === "--help" || command === "-h") {
|
|
270
|
+
printHelp();
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
if (command !== "create") {
|
|
274
|
+
throw new Error(`Unknown command: ${command}`);
|
|
275
|
+
}
|
|
276
|
+
let options = parseCreateArgs(rest);
|
|
277
|
+
let catalog = await loadCatalog(options.catalogUrl);
|
|
278
|
+
let starterName = options.starter || await promptForStarter(catalog);
|
|
279
|
+
let starter = catalog.find((entry) => entry.slug === starterName);
|
|
280
|
+
if (!starter) {
|
|
281
|
+
throw new Error(`Unknown starter: ${starterName}`);
|
|
282
|
+
}
|
|
283
|
+
if (!options.projectName) {
|
|
284
|
+
throw new Error("Project name is required");
|
|
285
|
+
}
|
|
286
|
+
let root = await createProject(starter, options);
|
|
287
|
+
console.log(`Created ${starter.slug} project in ${root}`);
|
|
288
|
+
}
|
|
289
|
+
function parseCreateArgs(argv) {
|
|
290
|
+
let options = {
|
|
291
|
+
install: true,
|
|
292
|
+
git: true
|
|
293
|
+
};
|
|
294
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
295
|
+
let arg = argv[index];
|
|
296
|
+
if (!arg) continue;
|
|
297
|
+
if (!arg.startsWith("--") && !options.projectName) {
|
|
298
|
+
options.projectName = arg;
|
|
299
|
+
continue;
|
|
300
|
+
}
|
|
301
|
+
if (arg === "--starter") {
|
|
302
|
+
if (!argv[index + 1]) throw new Error("--starter requires a value");
|
|
303
|
+
options.starter = argv[index + 1];
|
|
304
|
+
index += 1;
|
|
305
|
+
continue;
|
|
306
|
+
}
|
|
307
|
+
if (arg === "--no-install") {
|
|
308
|
+
options.install = false;
|
|
309
|
+
continue;
|
|
310
|
+
}
|
|
311
|
+
if (arg === "--no-git") {
|
|
312
|
+
options.git = false;
|
|
313
|
+
continue;
|
|
314
|
+
}
|
|
315
|
+
if (arg === "--catalog-url") {
|
|
316
|
+
if (!argv[index + 1]) throw new Error("--catalog-url requires a value");
|
|
317
|
+
options.catalogUrl = argv[index + 1];
|
|
318
|
+
index += 1;
|
|
319
|
+
continue;
|
|
320
|
+
}
|
|
321
|
+
throw new Error(`Unknown argument: ${arg}`);
|
|
322
|
+
}
|
|
323
|
+
return options;
|
|
324
|
+
}
|
|
325
|
+
function printHelp() {
|
|
326
|
+
console.log(
|
|
327
|
+
[
|
|
328
|
+
"Usage:",
|
|
329
|
+
" gistajs create <project-name> [--starter <slug>] [--no-install] [--no-git]"
|
|
330
|
+
].join("\n")
|
|
331
|
+
);
|
|
332
|
+
}
|
|
333
|
+
export {
|
|
334
|
+
createProject,
|
|
335
|
+
runCli
|
|
336
|
+
};
|
package/package.json
CHANGED
|
@@ -1 +1,55 @@
|
|
|
1
|
-
{
|
|
1
|
+
{
|
|
2
|
+
"name": "gistajs",
|
|
3
|
+
"version": "0.0.2",
|
|
4
|
+
"description": "Create Gista.js starter projects",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"cli",
|
|
7
|
+
"gistajs",
|
|
8
|
+
"scaffold",
|
|
9
|
+
"starter"
|
|
10
|
+
],
|
|
11
|
+
"license": "MIT",
|
|
12
|
+
"author": {
|
|
13
|
+
"name": "Kenn Ejima",
|
|
14
|
+
"email": "kenn@users.noreply.github.com"
|
|
15
|
+
},
|
|
16
|
+
"repository": {
|
|
17
|
+
"type": "git",
|
|
18
|
+
"url": "https://github.com/gistajs/gistajs.git"
|
|
19
|
+
},
|
|
20
|
+
"bin": {
|
|
21
|
+
"gistajs": "dist/bin.cjs"
|
|
22
|
+
},
|
|
23
|
+
"files": [
|
|
24
|
+
"dist",
|
|
25
|
+
"README.md"
|
|
26
|
+
],
|
|
27
|
+
"type": "module",
|
|
28
|
+
"sideEffects": false,
|
|
29
|
+
"exports": {
|
|
30
|
+
".": {
|
|
31
|
+
"import": "./dist/index.js",
|
|
32
|
+
"types": "./dist/index.d.ts"
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
"dependencies": {
|
|
36
|
+
"tar": "^7.5.1"
|
|
37
|
+
},
|
|
38
|
+
"devDependencies": {
|
|
39
|
+
"@types/node": "^25.5.0",
|
|
40
|
+
"np": "^11.0.2",
|
|
41
|
+
"oxfmt": "^0.40.0",
|
|
42
|
+
"tsup": "^8.5.1",
|
|
43
|
+
"typescript": "^5.9.3",
|
|
44
|
+
"vitest": "^4.1.0"
|
|
45
|
+
},
|
|
46
|
+
"engines": {
|
|
47
|
+
"node": ">=20"
|
|
48
|
+
},
|
|
49
|
+
"scripts": {
|
|
50
|
+
"build": "tsup",
|
|
51
|
+
"test": "vitest",
|
|
52
|
+
"typecheck": "tsc -b",
|
|
53
|
+
"np": "np"
|
|
54
|
+
}
|
|
55
|
+
}
|
package/.gitattributes
DELETED