create-macostack 0.1.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/README.md +53 -0
- package/bin/create-macostack.ts +507 -0
- package/package.json +28 -0
package/README.md
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# create-macostack
|
|
2
|
+
|
|
3
|
+
Orquestador mínimo para crear ventures macostack desde templates privados.
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
export GITHUB_TOKEN="github_pat_..." # fine-grained, Contents: Read-only
|
|
7
|
+
bun create macostack mi-tienda
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
Salida esperada:
|
|
11
|
+
|
|
12
|
+
```txt
|
|
13
|
+
mi-tienda/
|
|
14
|
+
server/ # repo git propio
|
|
15
|
+
client/ # repo git propio
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
La raíz no se inicializa como git. El CLI descarga tarballs privados, instala con
|
|
19
|
+
`bun install`, delega `bun run setup` cuando existe, y asegura un repo git por
|
|
20
|
+
mitad si el setup no lo creó.
|
|
21
|
+
|
|
22
|
+
## Desarrollo local
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
bun install
|
|
26
|
+
bun run dev mi-tienda
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Configuración
|
|
30
|
+
|
|
31
|
+
Defaults:
|
|
32
|
+
|
|
33
|
+
- owner: `macobits`
|
|
34
|
+
- server repo: `macostack-server`
|
|
35
|
+
- client repo: `macostack-client`
|
|
36
|
+
- ref: `main`
|
|
37
|
+
|
|
38
|
+
Flags útiles:
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
bun run dev mi-tienda --owner macobits --ref main
|
|
42
|
+
bun run dev mi-api --server --skip-setup
|
|
43
|
+
bun run dev mi-web --client --skip-install
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
También puedes usar env vars:
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
MACOSTACK_GITHUB_OWNER=macobits
|
|
50
|
+
MACOSTACK_SERVER_REPO=macostack-server
|
|
51
|
+
MACOSTACK_CLIENT_REPO=macostack-client
|
|
52
|
+
GITHUB_TOKEN=github_pat_...
|
|
53
|
+
```
|
|
@@ -0,0 +1,507 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
import {
|
|
3
|
+
cancel,
|
|
4
|
+
intro,
|
|
5
|
+
isCancel,
|
|
6
|
+
multiselect,
|
|
7
|
+
note,
|
|
8
|
+
outro,
|
|
9
|
+
spinner,
|
|
10
|
+
text,
|
|
11
|
+
} from "@clack/prompts";
|
|
12
|
+
import {
|
|
13
|
+
existsSync,
|
|
14
|
+
mkdirSync,
|
|
15
|
+
mkdtempSync,
|
|
16
|
+
readdirSync,
|
|
17
|
+
rmSync,
|
|
18
|
+
} from "node:fs";
|
|
19
|
+
import { tmpdir } from "node:os";
|
|
20
|
+
import { join, resolve } from "node:path";
|
|
21
|
+
import { parseArgs } from "node:util";
|
|
22
|
+
|
|
23
|
+
const STACK_PART = {
|
|
24
|
+
SERVER: "server",
|
|
25
|
+
CLIENT: "client",
|
|
26
|
+
} as const;
|
|
27
|
+
|
|
28
|
+
type StackPart = (typeof STACK_PART)[keyof typeof STACK_PART];
|
|
29
|
+
|
|
30
|
+
type TemplateConfig = {
|
|
31
|
+
part: StackPart;
|
|
32
|
+
directory: string;
|
|
33
|
+
repo: string;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
type ScaffoldConfig = {
|
|
37
|
+
owner: string;
|
|
38
|
+
ref: string;
|
|
39
|
+
token: string;
|
|
40
|
+
targetRoot: string;
|
|
41
|
+
targetName: string;
|
|
42
|
+
skipInstall: boolean;
|
|
43
|
+
skipSetup: boolean;
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
type DownloadTemplateProps = {
|
|
47
|
+
config: ScaffoldConfig;
|
|
48
|
+
template: TemplateConfig;
|
|
49
|
+
destination: string;
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
type RunCommandProps = {
|
|
53
|
+
command: string[];
|
|
54
|
+
cwd: string;
|
|
55
|
+
label: string;
|
|
56
|
+
allowFailure?: boolean;
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
type PackageJson = {
|
|
60
|
+
scripts?: Record<string, string>;
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
const DEFAULT_OWNER = "macobits";
|
|
64
|
+
const DEFAULT_REF = "main";
|
|
65
|
+
const DEFAULT_SERVER_REPO = "macostack-server";
|
|
66
|
+
const DEFAULT_CLIENT_REPO = "macostack-client";
|
|
67
|
+
|
|
68
|
+
const parsedArgs = parseArgs({
|
|
69
|
+
args: Bun.argv.slice(2),
|
|
70
|
+
allowPositionals: true,
|
|
71
|
+
options: {
|
|
72
|
+
help: { type: "boolean", default: false },
|
|
73
|
+
yes: { type: "boolean", default: false },
|
|
74
|
+
server: { type: "boolean", default: false },
|
|
75
|
+
client: { type: "boolean", default: false },
|
|
76
|
+
owner: { type: "string" },
|
|
77
|
+
ref: { type: "string" },
|
|
78
|
+
"server-repo": { type: "string" },
|
|
79
|
+
"client-repo": { type: "string" },
|
|
80
|
+
"skip-install": { type: "boolean", default: false },
|
|
81
|
+
"skip-setup": { type: "boolean", default: false },
|
|
82
|
+
},
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
const flags = parsedArgs.values;
|
|
86
|
+
|
|
87
|
+
const HELP_TEXT = `create-macostack
|
|
88
|
+
|
|
89
|
+
Usage:
|
|
90
|
+
bun create macostack <venture-folder>
|
|
91
|
+
bun run dev <venture-folder> [flags]
|
|
92
|
+
|
|
93
|
+
Flags:
|
|
94
|
+
--server scaffold only server
|
|
95
|
+
--client scaffold only client
|
|
96
|
+
--owner <owner> GitHub owner/org (default: macobits)
|
|
97
|
+
--ref <ref> Git ref/tag/branch (default: main)
|
|
98
|
+
--server-repo <repo> server template repo (default: macostack-server)
|
|
99
|
+
--client-repo <repo> client template repo (default: macostack-client)
|
|
100
|
+
--skip-install do not run bun install in generated halves
|
|
101
|
+
--skip-setup do not run bun run setup in generated halves
|
|
102
|
+
--yes skip create-macostack prompts; selects server + client
|
|
103
|
+
--help show this help
|
|
104
|
+
|
|
105
|
+
Auth:
|
|
106
|
+
Set GITHUB_TOKEN or run gh auth login for private template tarballs.
|
|
107
|
+
`;
|
|
108
|
+
|
|
109
|
+
if (flags.help === true) {
|
|
110
|
+
console.log(HELP_TEXT);
|
|
111
|
+
process.exit(0);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const bail = (message: string): never => {
|
|
115
|
+
cancel(message);
|
|
116
|
+
process.exit(1);
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
const answered = <T>(value: T | symbol): T => {
|
|
120
|
+
if (isCancel(value)) {
|
|
121
|
+
cancel("Scaffold aborted — nothing else will be touched.");
|
|
122
|
+
process.exit(0);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return value as T;
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
const getStringFlag = (
|
|
129
|
+
value: string | boolean | undefined,
|
|
130
|
+
fallback: string,
|
|
131
|
+
) => {
|
|
132
|
+
if (typeof value === "string" && value.length > 0) {
|
|
133
|
+
return value;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return fallback;
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
const getBooleanFlag = (value: string | boolean | undefined) => value === true;
|
|
140
|
+
|
|
141
|
+
const isRecord = (value: unknown): value is Record<string, unknown> =>
|
|
142
|
+
typeof value === "object" && value !== null;
|
|
143
|
+
|
|
144
|
+
const isValidDirectoryName = (value: string) =>
|
|
145
|
+
/^[a-z0-9][a-z0-9-]*$/.test(value);
|
|
146
|
+
|
|
147
|
+
const isStackPart = (value: string): value is StackPart =>
|
|
148
|
+
value === STACK_PART.SERVER || value === STACK_PART.CLIENT;
|
|
149
|
+
|
|
150
|
+
const readPackageJson = async (directory: string): Promise<PackageJson | null> => {
|
|
151
|
+
const packagePath = resolve(directory, "package.json");
|
|
152
|
+
if (!existsSync(packagePath)) {
|
|
153
|
+
return null;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const value: unknown = await Bun.file(packagePath).json();
|
|
157
|
+
if (!isRecord(value)) {
|
|
158
|
+
return null;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const scriptsValue = value.scripts;
|
|
162
|
+
if (!isRecord(scriptsValue)) {
|
|
163
|
+
return {};
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const scripts = Object.fromEntries(
|
|
167
|
+
Object.entries(scriptsValue).filter(
|
|
168
|
+
(entry): entry is [string, string] => typeof entry[1] === "string",
|
|
169
|
+
),
|
|
170
|
+
);
|
|
171
|
+
|
|
172
|
+
return { scripts };
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
const getGhToken = (): string | null => {
|
|
176
|
+
const result = Bun.spawnSync(["gh", "auth", "token"], {
|
|
177
|
+
stdout: "pipe",
|
|
178
|
+
stderr: "pipe",
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
if (result.exitCode !== 0) {
|
|
182
|
+
return null;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const token = result.stdout.toString().trim();
|
|
186
|
+
return token.length > 0 ? token : null;
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
const getGithubToken = (): string | null => {
|
|
190
|
+
const envToken = Bun.env.GITHUB_TOKEN ?? Bun.env.GH_TOKEN;
|
|
191
|
+
if (envToken && envToken.length > 0) {
|
|
192
|
+
return envToken;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
return getGhToken();
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
const requireGithubToken = (): string => {
|
|
199
|
+
const token = getGithubToken();
|
|
200
|
+
if (typeof token === "string" && token.length > 0) {
|
|
201
|
+
return token;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
return bail(
|
|
205
|
+
"Private templates require GitHub auth. Set GITHUB_TOKEN or run `gh auth login`. " +
|
|
206
|
+
"Use a fine-grained token with Contents: Read-only.",
|
|
207
|
+
);
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
const ensureEmptyDirectory = (directory: string, label: string) => {
|
|
211
|
+
if (!existsSync(directory)) {
|
|
212
|
+
mkdirSync(directory, { recursive: true });
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const entries = readdirSync(directory).filter((entry) => entry !== ".DS_Store");
|
|
217
|
+
if (entries.length > 0) {
|
|
218
|
+
bail(`${label} already exists and is not empty: ${directory}`);
|
|
219
|
+
}
|
|
220
|
+
};
|
|
221
|
+
|
|
222
|
+
const runCommand = ({
|
|
223
|
+
command,
|
|
224
|
+
cwd,
|
|
225
|
+
label,
|
|
226
|
+
allowFailure = false,
|
|
227
|
+
}: RunCommandProps) => {
|
|
228
|
+
const result = Bun.spawnSync(command, {
|
|
229
|
+
cwd,
|
|
230
|
+
stdout: "inherit",
|
|
231
|
+
stderr: "inherit",
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
if (result.exitCode === 0 || allowFailure) {
|
|
235
|
+
return result.exitCode;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
bail(`${label} failed with exit code ${result.exitCode}.`);
|
|
239
|
+
};
|
|
240
|
+
|
|
241
|
+
const downloadTemplate = async ({
|
|
242
|
+
config,
|
|
243
|
+
template,
|
|
244
|
+
destination,
|
|
245
|
+
}: DownloadTemplateProps) => {
|
|
246
|
+
const temporaryDirectory = mkdtempSync(join(tmpdir(), "create-macostack-"));
|
|
247
|
+
const archivePath = join(temporaryDirectory, `${template.part}.tar.gz`);
|
|
248
|
+
const tarballUrl = `https://api.github.com/repos/${config.owner}/${template.repo}/tarball/${config.ref}`;
|
|
249
|
+
|
|
250
|
+
try {
|
|
251
|
+
const response = await fetch(tarballUrl, {
|
|
252
|
+
headers: {
|
|
253
|
+
Accept: "application/vnd.github+json",
|
|
254
|
+
Authorization: `Bearer ${config.token}`,
|
|
255
|
+
"User-Agent": "create-macostack",
|
|
256
|
+
"X-GitHub-Api-Version": "2022-11-28",
|
|
257
|
+
},
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
if (!response.ok) {
|
|
261
|
+
const details = await response.text();
|
|
262
|
+
bail(
|
|
263
|
+
`GitHub refused ${config.owner}/${template.repo}@${config.ref} (${response.status}). ` +
|
|
264
|
+
`Check GITHUB_TOKEN permissions. ${details.slice(0, 240)}`,
|
|
265
|
+
);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
await Bun.write(archivePath, await response.arrayBuffer());
|
|
269
|
+
ensureEmptyDirectory(destination, template.directory);
|
|
270
|
+
runCommand({
|
|
271
|
+
command: [
|
|
272
|
+
"tar",
|
|
273
|
+
"-xzf",
|
|
274
|
+
archivePath,
|
|
275
|
+
"-C",
|
|
276
|
+
destination,
|
|
277
|
+
"--strip-components=1",
|
|
278
|
+
],
|
|
279
|
+
cwd: destination,
|
|
280
|
+
label: `Extract ${template.part}`,
|
|
281
|
+
});
|
|
282
|
+
} finally {
|
|
283
|
+
rmSync(temporaryDirectory, { recursive: true, force: true });
|
|
284
|
+
}
|
|
285
|
+
};
|
|
286
|
+
|
|
287
|
+
const runInstall = (directory: string, template: TemplateConfig) => {
|
|
288
|
+
runCommand({
|
|
289
|
+
command: ["bun", "install"],
|
|
290
|
+
cwd: directory,
|
|
291
|
+
label: `bun install in ${template.part}`,
|
|
292
|
+
});
|
|
293
|
+
};
|
|
294
|
+
|
|
295
|
+
const runSetup = async (directory: string, template: TemplateConfig) => {
|
|
296
|
+
const packageJson = await readPackageJson(directory);
|
|
297
|
+
if (!packageJson?.scripts?.setup) {
|
|
298
|
+
note(`No setup script found in ${template.directory}; skipping delegation.`, template.part);
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
runCommand({
|
|
303
|
+
command: ["bun", "run", "setup"],
|
|
304
|
+
cwd: directory,
|
|
305
|
+
label: `setup in ${template.part}`,
|
|
306
|
+
});
|
|
307
|
+
};
|
|
308
|
+
|
|
309
|
+
const ensureGitRepository = (directory: string, template: TemplateConfig) => {
|
|
310
|
+
if (existsSync(resolve(directory, ".git"))) {
|
|
311
|
+
return;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
runCommand({
|
|
315
|
+
command: ["git", "init", "-b", "main"],
|
|
316
|
+
cwd: directory,
|
|
317
|
+
label: `git init in ${template.part}`,
|
|
318
|
+
});
|
|
319
|
+
runCommand({
|
|
320
|
+
command: ["git", "add", "-A"],
|
|
321
|
+
cwd: directory,
|
|
322
|
+
label: `git add in ${template.part}`,
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
const commitExitCode = runCommand({
|
|
326
|
+
command: ["git", "commit", "-m", "init from template"],
|
|
327
|
+
cwd: directory,
|
|
328
|
+
label: `git commit in ${template.part}`,
|
|
329
|
+
allowFailure: true,
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
if (commitExitCode !== 0) {
|
|
333
|
+
note(
|
|
334
|
+
`Fresh ${template.part} repo created, but initial commit failed. ` +
|
|
335
|
+
`Run: cd ${template.directory} && git add -A && git commit -m "init"`,
|
|
336
|
+
"Git",
|
|
337
|
+
);
|
|
338
|
+
}
|
|
339
|
+
};
|
|
340
|
+
|
|
341
|
+
const getSelectedPartsFromFlags = () => {
|
|
342
|
+
const selected: StackPart[] = [];
|
|
343
|
+
|
|
344
|
+
if (getBooleanFlag(flags.server)) {
|
|
345
|
+
selected.push(STACK_PART.SERVER);
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
if (getBooleanFlag(flags.client)) {
|
|
349
|
+
selected.push(STACK_PART.CLIENT);
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
return selected;
|
|
353
|
+
};
|
|
354
|
+
|
|
355
|
+
const resolveTargetName = async () => {
|
|
356
|
+
const positionalTarget = parsedArgs.positionals[0];
|
|
357
|
+
if (positionalTarget) {
|
|
358
|
+
if (!isValidDirectoryName(positionalTarget)) {
|
|
359
|
+
bail("Project folder must use lowercase letters, digits and dashes only.");
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
return positionalTarget;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
if (getBooleanFlag(flags.yes)) {
|
|
366
|
+
bail("Missing project folder. Usage: bun create macostack my-venture");
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
const value = answered(
|
|
370
|
+
await text({
|
|
371
|
+
message: "Venture folder",
|
|
372
|
+
placeholder: "mi-tienda",
|
|
373
|
+
validate: (input) =>
|
|
374
|
+
isValidDirectoryName(input ?? "")
|
|
375
|
+
? undefined
|
|
376
|
+
: "lowercase letters, digits and dashes only",
|
|
377
|
+
}),
|
|
378
|
+
);
|
|
379
|
+
|
|
380
|
+
return value;
|
|
381
|
+
};
|
|
382
|
+
|
|
383
|
+
const resolveSelectedParts = async () => {
|
|
384
|
+
const selectedFromFlags = getSelectedPartsFromFlags();
|
|
385
|
+
if (selectedFromFlags.length > 0) {
|
|
386
|
+
return selectedFromFlags;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
if (getBooleanFlag(flags.yes)) {
|
|
390
|
+
return [STACK_PART.SERVER, STACK_PART.CLIENT];
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
const selected = answered(
|
|
394
|
+
await multiselect({
|
|
395
|
+
message: "¿Qué incluye este venture?",
|
|
396
|
+
options: [
|
|
397
|
+
{ value: STACK_PART.SERVER, label: "server" },
|
|
398
|
+
{ value: STACK_PART.CLIENT, label: "client" },
|
|
399
|
+
],
|
|
400
|
+
initialValues: [STACK_PART.SERVER, STACK_PART.CLIENT],
|
|
401
|
+
required: true,
|
|
402
|
+
}),
|
|
403
|
+
);
|
|
404
|
+
|
|
405
|
+
const validSelected = selected.filter(isStackPart);
|
|
406
|
+
if (validSelected.length === 0) {
|
|
407
|
+
bail("Select at least one half.");
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
return validSelected;
|
|
411
|
+
};
|
|
412
|
+
|
|
413
|
+
const buildTemplates = (selectedParts: StackPart[]) => {
|
|
414
|
+
const serverRepo = getStringFlag(
|
|
415
|
+
flags["server-repo"],
|
|
416
|
+
Bun.env.MACOSTACK_SERVER_REPO ?? DEFAULT_SERVER_REPO,
|
|
417
|
+
);
|
|
418
|
+
const clientRepo = getStringFlag(
|
|
419
|
+
flags["client-repo"],
|
|
420
|
+
Bun.env.MACOSTACK_CLIENT_REPO ?? DEFAULT_CLIENT_REPO,
|
|
421
|
+
);
|
|
422
|
+
|
|
423
|
+
const templates = {
|
|
424
|
+
[STACK_PART.SERVER]: {
|
|
425
|
+
part: STACK_PART.SERVER,
|
|
426
|
+
directory: "server",
|
|
427
|
+
repo: serverRepo,
|
|
428
|
+
},
|
|
429
|
+
[STACK_PART.CLIENT]: {
|
|
430
|
+
part: STACK_PART.CLIENT,
|
|
431
|
+
directory: "client",
|
|
432
|
+
repo: clientRepo,
|
|
433
|
+
},
|
|
434
|
+
} satisfies Record<StackPart, TemplateConfig>;
|
|
435
|
+
|
|
436
|
+
return selectedParts.map((part) => templates[part]);
|
|
437
|
+
};
|
|
438
|
+
|
|
439
|
+
const scaffoldTemplate = async (config: ScaffoldConfig, template: TemplateConfig) => {
|
|
440
|
+
const destination = resolve(config.targetRoot, template.directory);
|
|
441
|
+
const s = spinner();
|
|
442
|
+
|
|
443
|
+
s.start(`Downloading ${config.owner}/${template.repo}@${config.ref} → ${template.directory}`);
|
|
444
|
+
await downloadTemplate({ config, template, destination });
|
|
445
|
+
s.stop(`${template.directory} downloaded`);
|
|
446
|
+
|
|
447
|
+
if (!config.skipInstall) {
|
|
448
|
+
runInstall(destination, template);
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
if (!config.skipSetup) {
|
|
452
|
+
await runSetup(destination, template);
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
ensureGitRepository(destination, template);
|
|
456
|
+
};
|
|
457
|
+
|
|
458
|
+
const main = async () => {
|
|
459
|
+
intro("create-macostack");
|
|
460
|
+
|
|
461
|
+
const targetName = await resolveTargetName();
|
|
462
|
+
const selectedParts = await resolveSelectedParts();
|
|
463
|
+
const owner = getStringFlag(
|
|
464
|
+
flags.owner,
|
|
465
|
+
Bun.env.MACOSTACK_GITHUB_OWNER ?? DEFAULT_OWNER,
|
|
466
|
+
);
|
|
467
|
+
const ref = getStringFlag(flags.ref, Bun.env.MACOSTACK_REF ?? DEFAULT_REF);
|
|
468
|
+
const githubToken = requireGithubToken();
|
|
469
|
+
|
|
470
|
+
const config: ScaffoldConfig = {
|
|
471
|
+
owner,
|
|
472
|
+
ref,
|
|
473
|
+
token: githubToken,
|
|
474
|
+
targetRoot: resolve(process.cwd(), targetName),
|
|
475
|
+
targetName,
|
|
476
|
+
skipInstall: getBooleanFlag(flags["skip-install"]),
|
|
477
|
+
skipSetup: getBooleanFlag(flags["skip-setup"]),
|
|
478
|
+
};
|
|
479
|
+
const templates = buildTemplates(selectedParts);
|
|
480
|
+
|
|
481
|
+
ensureEmptyDirectory(config.targetRoot, "Target root");
|
|
482
|
+
|
|
483
|
+
for (const template of templates) {
|
|
484
|
+
await scaffoldTemplate(config, template);
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
const remoteSteps = templates
|
|
488
|
+
.map(
|
|
489
|
+
(template) =>
|
|
490
|
+
` cd ${config.targetName}/${template.directory}\n` +
|
|
491
|
+
` git remote add origin <${template.part}-repo-url>\n` +
|
|
492
|
+
" git push -u origin main",
|
|
493
|
+
)
|
|
494
|
+
.join("\n\n");
|
|
495
|
+
|
|
496
|
+
const devSteps = templates
|
|
497
|
+
.map((template) => ` cd ${config.targetName}/${template.directory} && bun dev`)
|
|
498
|
+
.join("\n");
|
|
499
|
+
|
|
500
|
+
outro(
|
|
501
|
+
`Done. Root folder is intentionally not a git repo.\n\n` +
|
|
502
|
+
`Next remotes:\n${remoteSteps}\n\n` +
|
|
503
|
+
`Dev:\n${devSteps}`,
|
|
504
|
+
);
|
|
505
|
+
};
|
|
506
|
+
|
|
507
|
+
await main();
|
package/package.json
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "create-macostack",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Private macostack scaffold orchestrator for detached server/client repos.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"create-macostack": "./bin/create-macostack.ts"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"bin",
|
|
11
|
+
"README.md"
|
|
12
|
+
],
|
|
13
|
+
"scripts": {
|
|
14
|
+
"dev": "bun run bin/create-macostack.ts",
|
|
15
|
+
"start": "bun run bin/create-macostack.ts",
|
|
16
|
+
"typecheck": "bunx tsc --noEmit"
|
|
17
|
+
},
|
|
18
|
+
"publishConfig": {
|
|
19
|
+
"access": "public"
|
|
20
|
+
},
|
|
21
|
+
"dependencies": {
|
|
22
|
+
"@clack/prompts": "^1.7.0"
|
|
23
|
+
},
|
|
24
|
+
"devDependencies": {
|
|
25
|
+
"@types/bun": "^1.3.14",
|
|
26
|
+
"typescript": "^6.0.3"
|
|
27
|
+
}
|
|
28
|
+
}
|