schub 0.1.0 → 0.1.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 +68 -0
- package/dist/index.js +1573 -597
- package/package.json +3 -1
- package/skills/create-proposal/SKILL.md +33 -0
- package/skills/create-tasks/SKILL.md +40 -0
- package/skills/implement-task/SKILL.md +84 -0
- package/skills/review-proposal/SKILL.md +37 -0
- package/skills/setup-project/SKILL.md +29 -0
- package/src/App.test.tsx +93 -0
- package/src/App.tsx +62 -10
- package/src/changes.ts +86 -28
- package/src/clipboard.ts +5 -0
- package/src/commands/adr.test.ts +69 -0
- package/src/commands/adr.ts +107 -0
- package/src/commands/changes.test.ts +171 -0
- package/src/commands/changes.ts +163 -0
- package/src/commands/cookbook.test.ts +71 -0
- package/src/commands/cookbook.ts +95 -0
- package/src/commands/eject.test.ts +74 -0
- package/src/commands/eject.ts +100 -0
- package/src/commands/init.test.ts +78 -0
- package/src/commands/init.ts +144 -0
- package/src/commands/project.test.ts +113 -0
- package/src/commands/project.ts +75 -0
- package/src/commands/review.test.ts +100 -0
- package/src/commands/review.ts +231 -0
- package/src/commands/tasks-create.test.ts +172 -0
- package/src/commands/tasks-list.test.ts +177 -0
- package/src/commands/tasks.ts +172 -0
- package/src/components/PlanView.test.tsx +113 -0
- package/src/components/PlanView.tsx +95 -26
- package/src/components/StatusView.test.tsx +380 -0
- package/src/components/StatusView.tsx +233 -83
- package/src/features/tasks/constants.ts +2 -0
- package/src/features/tasks/create.ts +15 -7
- package/src/features/tasks/filesystem.test.ts +78 -0
- package/src/features/tasks/filesystem.ts +61 -7
- package/src/ide.ts +7 -0
- package/src/index.test.ts +23 -0
- package/src/index.ts +60 -383
- package/src/init.test.ts +43 -0
- package/src/init.ts +27 -0
- package/src/project.ts +5 -32
- package/src/schub-root.ts +33 -0
- package/src/templates.ts +18 -0
- package/src/terminal.test.ts +46 -0
- package/templates/create-proposal/cookbook-template.md +37 -0
- package/templates/review-proposal/q&a-template.md +5 -1
- package/templates/templates-parity.test.ts +45 -0
- package/templates/setup-project/review-me-template.md +0 -18
package/src/index.ts
CHANGED
|
@@ -1,391 +1,44 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
|
-
import { dirname } from "node:path";
|
|
3
|
-
import { spawnSync } from "bun";
|
|
4
2
|
import { render } from "ink";
|
|
5
3
|
import React from "react";
|
|
6
4
|
import App from "./App";
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
5
|
+
import { runAdrCreate } from "./commands/adr";
|
|
6
|
+
import { runChangesCreate, runChangesStatus } from "./commands/changes";
|
|
7
|
+
import { runCookbookCreate } from "./commands/cookbook";
|
|
8
|
+
import { runEject } from "./commands/eject";
|
|
9
|
+
import { runInit } from "./commands/init";
|
|
10
|
+
import { runProjectCreate } from "./commands/project";
|
|
11
|
+
import { runReviewComplete, runReviewCreate } from "./commands/review";
|
|
12
|
+
import { runTasksCreate, runTasksList } from "./commands/tasks";
|
|
13
|
+
import { findSchubRoot } from "./features/tasks";
|
|
10
14
|
import { applyTerminalPrelude, applyTerminalReset } from "./terminal";
|
|
11
15
|
|
|
12
16
|
const HELP_TEXT = `schub [command]
|
|
13
17
|
|
|
14
18
|
Commands:
|
|
15
|
-
changes create
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
tasks
|
|
19
|
-
|
|
20
|
-
|
|
19
|
+
changes create Create a change proposal
|
|
20
|
+
changes status Update change proposal status
|
|
21
|
+
project create Create project docs
|
|
22
|
+
tasks create Create task files for a change
|
|
23
|
+
tasks list List tasks
|
|
24
|
+
review create Create REVIEW_ME for a change
|
|
25
|
+
review complete Create Q&A from REVIEW_ME
|
|
26
|
+
adr create Create an ADR for a change
|
|
27
|
+
cookbook create Create a cookbook for a change
|
|
28
|
+
init Initialize .schub and install Codex skills
|
|
29
|
+
eject Copy bundled skills and templates into .schub
|
|
21
30
|
ui Launch the interactive dashboard
|
|
22
31
|
`;
|
|
23
32
|
|
|
24
|
-
const getStartDir = ()
|
|
33
|
+
const getStartDir = () => process.env.SCHUB_CWD ?? process.cwd();
|
|
25
34
|
|
|
26
|
-
const
|
|
27
|
-
const schubDir = findSchubRoot(getStartDir());
|
|
28
|
-
return schubDir ? dirname(schubDir) : getStartDir();
|
|
29
|
-
};
|
|
30
|
-
|
|
31
|
-
const resolveSchubDir = (): string | null => findSchubRoot(getStartDir());
|
|
35
|
+
const resolveSchubDir = () => findSchubRoot(getStartDir());
|
|
32
36
|
|
|
33
|
-
const printHelp = (exitCode = 0)
|
|
37
|
+
const printHelp = (exitCode = 0) => {
|
|
34
38
|
process.stdout.write(`${HELP_TEXT}\n`);
|
|
35
39
|
process.exitCode = exitCode;
|
|
36
40
|
};
|
|
37
41
|
|
|
38
|
-
const parseStatusFilter = (value: string | undefined): TaskStatus[] => {
|
|
39
|
-
if (!value) {
|
|
40
|
-
return [...TASK_STATUSES];
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
const normalized = value
|
|
44
|
-
.split(",")
|
|
45
|
-
.map((status) => status.trim().toLowerCase())
|
|
46
|
-
.filter(Boolean);
|
|
47
|
-
|
|
48
|
-
const allowed = new Set(TASK_STATUSES);
|
|
49
|
-
const invalid = normalized.filter((status) => !allowed.has(status as TaskStatus));
|
|
50
|
-
if (invalid.length > 0) {
|
|
51
|
-
throw new Error(`Unknown status filter: ${invalid.join(", ")}`);
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
return normalized as TaskStatus[];
|
|
55
|
-
};
|
|
56
|
-
|
|
57
|
-
type TaskListOptions = {
|
|
58
|
-
statuses: TaskStatus[];
|
|
59
|
-
json: boolean;
|
|
60
|
-
};
|
|
61
|
-
|
|
62
|
-
const parseTaskListOptions = (args: string[]): TaskListOptions => {
|
|
63
|
-
let statusValue: string | undefined;
|
|
64
|
-
let json = false;
|
|
65
|
-
const unknown: string[] = [];
|
|
66
|
-
|
|
67
|
-
for (let index = 0; index < args.length; index += 1) {
|
|
68
|
-
const arg = args[index];
|
|
69
|
-
if (arg === "--json") {
|
|
70
|
-
json = true;
|
|
71
|
-
continue;
|
|
72
|
-
}
|
|
73
|
-
if (arg === "--status") {
|
|
74
|
-
statusValue = args[index + 1];
|
|
75
|
-
if (!statusValue) {
|
|
76
|
-
throw new Error("Missing value for --status.");
|
|
77
|
-
}
|
|
78
|
-
index += 1;
|
|
79
|
-
continue;
|
|
80
|
-
}
|
|
81
|
-
if (arg.startsWith("--status=")) {
|
|
82
|
-
statusValue = arg.slice("--status=".length);
|
|
83
|
-
continue;
|
|
84
|
-
}
|
|
85
|
-
unknown.push(arg);
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
if (unknown.length > 0) {
|
|
89
|
-
throw new Error(`Unknown option(s): ${unknown.join(", ")}`);
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
return { statuses: parseStatusFilter(statusValue), json };
|
|
93
|
-
};
|
|
94
|
-
|
|
95
|
-
type ChangeCreateOptions = {
|
|
96
|
-
changeId?: string;
|
|
97
|
-
title?: string;
|
|
98
|
-
input?: string;
|
|
99
|
-
overwrite: boolean;
|
|
100
|
-
};
|
|
101
|
-
|
|
102
|
-
type ProjectCreateOptions = {
|
|
103
|
-
repoRoot?: string;
|
|
104
|
-
projectName?: string;
|
|
105
|
-
overwrite: boolean;
|
|
106
|
-
};
|
|
107
|
-
|
|
108
|
-
const parseChangeCreateOptions = (args: string[]) => {
|
|
109
|
-
let changeId: string | undefined;
|
|
110
|
-
let title: string | undefined;
|
|
111
|
-
let input: string | undefined;
|
|
112
|
-
let overwrite = false;
|
|
113
|
-
const unknown: string[] = [];
|
|
114
|
-
|
|
115
|
-
const rejectUnsupported = (flag: string) => {
|
|
116
|
-
throw new Error(`Unsupported option: ${flag}.`);
|
|
117
|
-
};
|
|
118
|
-
|
|
119
|
-
for (let index = 0; index < args.length; index += 1) {
|
|
120
|
-
const arg = args[index];
|
|
121
|
-
if (arg === "--overwrite") {
|
|
122
|
-
overwrite = true;
|
|
123
|
-
continue;
|
|
124
|
-
}
|
|
125
|
-
if (arg === "--change-id") {
|
|
126
|
-
changeId = args[index + 1];
|
|
127
|
-
if (changeId === undefined) {
|
|
128
|
-
throw new Error("Missing value for --change-id.");
|
|
129
|
-
}
|
|
130
|
-
index += 1;
|
|
131
|
-
continue;
|
|
132
|
-
}
|
|
133
|
-
if (arg.startsWith("--change-id=")) {
|
|
134
|
-
changeId = arg.slice("--change-id=".length);
|
|
135
|
-
continue;
|
|
136
|
-
}
|
|
137
|
-
if (arg === "--title") {
|
|
138
|
-
title = args[index + 1];
|
|
139
|
-
if (title === undefined) {
|
|
140
|
-
throw new Error("Missing value for --title.");
|
|
141
|
-
}
|
|
142
|
-
index += 1;
|
|
143
|
-
continue;
|
|
144
|
-
}
|
|
145
|
-
if (arg.startsWith("--title=")) {
|
|
146
|
-
title = arg.slice("--title=".length);
|
|
147
|
-
continue;
|
|
148
|
-
}
|
|
149
|
-
if (arg === "--input") {
|
|
150
|
-
input = args[index + 1];
|
|
151
|
-
if (input === undefined) {
|
|
152
|
-
throw new Error("Missing value for --input.");
|
|
153
|
-
}
|
|
154
|
-
index += 1;
|
|
155
|
-
continue;
|
|
156
|
-
}
|
|
157
|
-
if (arg.startsWith("--input=")) {
|
|
158
|
-
input = arg.slice("--input=".length);
|
|
159
|
-
continue;
|
|
160
|
-
}
|
|
161
|
-
if (arg === "--schub-root" || arg === "--agent-root") {
|
|
162
|
-
rejectUnsupported(arg);
|
|
163
|
-
}
|
|
164
|
-
if (arg.startsWith("--schub-root=")) {
|
|
165
|
-
rejectUnsupported("--schub-root");
|
|
166
|
-
}
|
|
167
|
-
if (arg.startsWith("--agent-root=")) {
|
|
168
|
-
rejectUnsupported("--agent-root");
|
|
169
|
-
}
|
|
170
|
-
unknown.push(arg);
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
if (unknown.length > 0) {
|
|
174
|
-
throw new Error(`Unknown option(s): ${unknown.join(", ")}`);
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
const options: ChangeCreateOptions = { changeId, title, input, overwrite };
|
|
178
|
-
return options;
|
|
179
|
-
};
|
|
180
|
-
|
|
181
|
-
const parseProjectCreateOptions = (args: string[]) => {
|
|
182
|
-
let repoRoot: string | undefined;
|
|
183
|
-
let projectName: string | undefined;
|
|
184
|
-
let overwrite = false;
|
|
185
|
-
const unknown: string[] = [];
|
|
186
|
-
|
|
187
|
-
const rejectUnsupported = (flag: string) => {
|
|
188
|
-
throw new Error(`Unsupported option: ${flag}.`);
|
|
189
|
-
};
|
|
190
|
-
|
|
191
|
-
for (let index = 0; index < args.length; index += 1) {
|
|
192
|
-
const arg = args[index];
|
|
193
|
-
if (arg === "--overwrite") {
|
|
194
|
-
overwrite = true;
|
|
195
|
-
continue;
|
|
196
|
-
}
|
|
197
|
-
if (arg === "--repo-root") {
|
|
198
|
-
repoRoot = args[index + 1];
|
|
199
|
-
if (repoRoot === undefined) {
|
|
200
|
-
throw new Error("Missing value for --repo-root.");
|
|
201
|
-
}
|
|
202
|
-
index += 1;
|
|
203
|
-
continue;
|
|
204
|
-
}
|
|
205
|
-
if (arg.startsWith("--repo-root=")) {
|
|
206
|
-
repoRoot = arg.slice("--repo-root=".length);
|
|
207
|
-
continue;
|
|
208
|
-
}
|
|
209
|
-
if (arg === "--project-name") {
|
|
210
|
-
projectName = args[index + 1];
|
|
211
|
-
if (projectName === undefined) {
|
|
212
|
-
throw new Error("Missing value for --project-name.");
|
|
213
|
-
}
|
|
214
|
-
index += 1;
|
|
215
|
-
continue;
|
|
216
|
-
}
|
|
217
|
-
if (arg.startsWith("--project-name=")) {
|
|
218
|
-
projectName = arg.slice("--project-name=".length);
|
|
219
|
-
continue;
|
|
220
|
-
}
|
|
221
|
-
if (arg === "--schub-root" || arg === "--agent-root") {
|
|
222
|
-
rejectUnsupported(arg);
|
|
223
|
-
}
|
|
224
|
-
if (arg.startsWith("--schub-root=")) {
|
|
225
|
-
rejectUnsupported("--schub-root");
|
|
226
|
-
}
|
|
227
|
-
if (arg.startsWith("--agent-root=")) {
|
|
228
|
-
rejectUnsupported("--agent-root");
|
|
229
|
-
}
|
|
230
|
-
unknown.push(arg);
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
if (unknown.length > 0) {
|
|
234
|
-
throw new Error(`Unknown option(s): ${unknown.join(", ")}`);
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
return { repoRoot, projectName, overwrite };
|
|
238
|
-
};
|
|
239
|
-
|
|
240
|
-
type TaskCreateOptions = {
|
|
241
|
-
changeId: string;
|
|
242
|
-
status?: string;
|
|
243
|
-
titles: string[];
|
|
244
|
-
overwrite: boolean;
|
|
245
|
-
};
|
|
246
|
-
|
|
247
|
-
const parseTaskCreateOptions = (args: string[]) => {
|
|
248
|
-
let changeId: string | undefined;
|
|
249
|
-
let status: string | undefined;
|
|
250
|
-
let overwrite = false;
|
|
251
|
-
const titles: string[] = [];
|
|
252
|
-
const unknown: string[] = [];
|
|
253
|
-
|
|
254
|
-
const rejectUnsupported = (flag: string) => {
|
|
255
|
-
throw new Error(`Unsupported option: ${flag}.`);
|
|
256
|
-
};
|
|
257
|
-
|
|
258
|
-
for (let index = 0; index < args.length; index += 1) {
|
|
259
|
-
const arg = args[index];
|
|
260
|
-
if (arg === "--overwrite") {
|
|
261
|
-
overwrite = true;
|
|
262
|
-
continue;
|
|
263
|
-
}
|
|
264
|
-
if (arg === "--change-id") {
|
|
265
|
-
changeId = args[index + 1];
|
|
266
|
-
if (changeId === undefined) {
|
|
267
|
-
throw new Error("Missing value for --change-id.");
|
|
268
|
-
}
|
|
269
|
-
index += 1;
|
|
270
|
-
continue;
|
|
271
|
-
}
|
|
272
|
-
if (arg.startsWith("--change-id=")) {
|
|
273
|
-
changeId = arg.slice("--change-id=".length);
|
|
274
|
-
continue;
|
|
275
|
-
}
|
|
276
|
-
if (arg === "--status") {
|
|
277
|
-
status = args[index + 1];
|
|
278
|
-
if (status === undefined) {
|
|
279
|
-
throw new Error("Missing value for --status.");
|
|
280
|
-
}
|
|
281
|
-
index += 1;
|
|
282
|
-
continue;
|
|
283
|
-
}
|
|
284
|
-
if (arg.startsWith("--status=")) {
|
|
285
|
-
status = arg.slice("--status=".length);
|
|
286
|
-
continue;
|
|
287
|
-
}
|
|
288
|
-
if (arg === "--title") {
|
|
289
|
-
const title = args[index + 1];
|
|
290
|
-
if (title === undefined) {
|
|
291
|
-
throw new Error("Missing value for --title.");
|
|
292
|
-
}
|
|
293
|
-
titles.push(title);
|
|
294
|
-
index += 1;
|
|
295
|
-
continue;
|
|
296
|
-
}
|
|
297
|
-
if (arg.startsWith("--title=")) {
|
|
298
|
-
titles.push(arg.slice("--title=".length));
|
|
299
|
-
continue;
|
|
300
|
-
}
|
|
301
|
-
if (arg === "--schub-root" || arg === "--agent-root") {
|
|
302
|
-
rejectUnsupported(arg);
|
|
303
|
-
}
|
|
304
|
-
if (arg.startsWith("--schub-root=")) {
|
|
305
|
-
rejectUnsupported("--schub-root");
|
|
306
|
-
}
|
|
307
|
-
if (arg.startsWith("--agent-root=")) {
|
|
308
|
-
rejectUnsupported("--agent-root");
|
|
309
|
-
}
|
|
310
|
-
unknown.push(arg);
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
if (unknown.length > 0) {
|
|
314
|
-
throw new Error(`Unknown option(s): ${unknown.join(", ")}`);
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
if (!changeId) {
|
|
318
|
-
throw new Error("Provide --change-id.");
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
const options: TaskCreateOptions = { changeId, status, titles, overwrite };
|
|
322
|
-
return options;
|
|
323
|
-
};
|
|
324
|
-
|
|
325
|
-
const runTasksList = (args: string[]): void => {
|
|
326
|
-
const schubDir = resolveSchubDir();
|
|
327
|
-
if (!schubDir) {
|
|
328
|
-
throw new Error("No .schub directory found.");
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
const options = parseTaskListOptions(args);
|
|
332
|
-
const tasks = listTasks(schubDir, options.statuses);
|
|
333
|
-
|
|
334
|
-
if (options.json) {
|
|
335
|
-
process.stdout.write(`${JSON.stringify(tasks, null, 2)}\n`);
|
|
336
|
-
return;
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
const lines = tasks.map((task) => `${task.id} ${task.title} (${task.status})`);
|
|
340
|
-
process.stdout.write(`${lines.join("\n")}\n`);
|
|
341
|
-
};
|
|
342
|
-
|
|
343
|
-
const runTasksCreate = (args: string[]) => {
|
|
344
|
-
const options = parseTaskCreateOptions(args);
|
|
345
|
-
const schubDir = resolveChangeRoot(getStartDir());
|
|
346
|
-
const created = createTask(schubDir, options);
|
|
347
|
-
|
|
348
|
-
for (const taskPath of created) {
|
|
349
|
-
process.stdout.write(`[OK] Wrote task: ${taskPath}\n`);
|
|
350
|
-
}
|
|
351
|
-
};
|
|
352
|
-
|
|
353
|
-
const runBiome = (command: "lint" | "format"): void => {
|
|
354
|
-
const workspaceRoot = getWorkspaceRoot();
|
|
355
|
-
const biomeArgs = command === "lint" ? ["lint", "."] : ["format", ".", "--write"];
|
|
356
|
-
const result = spawnSync({
|
|
357
|
-
cmd: ["bunx", "@biomejs/biome", ...biomeArgs],
|
|
358
|
-
cwd: workspaceRoot,
|
|
359
|
-
env: process.env,
|
|
360
|
-
});
|
|
361
|
-
|
|
362
|
-
if (result.stdout && result.stdout.length > 0) {
|
|
363
|
-
process.stdout.write(result.stdout);
|
|
364
|
-
}
|
|
365
|
-
|
|
366
|
-
if (result.stderr && result.stderr.length > 0) {
|
|
367
|
-
process.stderr.write(result.stderr);
|
|
368
|
-
}
|
|
369
|
-
|
|
370
|
-
process.exitCode = result.exitCode ?? 1;
|
|
371
|
-
};
|
|
372
|
-
|
|
373
|
-
const runChangesCreate = (args: string[]) => {
|
|
374
|
-
const options = parseChangeCreateOptions(args);
|
|
375
|
-
const schubDir = resolveChangeRoot(getStartDir());
|
|
376
|
-
const proposalPath = createChange(schubDir, options);
|
|
377
|
-
process.stdout.write(`[OK] Wrote proposal: ${proposalPath}\n`);
|
|
378
|
-
};
|
|
379
|
-
|
|
380
|
-
const runProjectCreate = (args: string[]) => {
|
|
381
|
-
const options = parseProjectCreateOptions(args);
|
|
382
|
-
const outputs = createProject(getStartDir(), options);
|
|
383
|
-
|
|
384
|
-
for (const output of outputs) {
|
|
385
|
-
process.stdout.write(`[OK] Wrote ${output}\n`);
|
|
386
|
-
}
|
|
387
|
-
};
|
|
388
|
-
|
|
389
42
|
const registerTerminalReset = () => {
|
|
390
43
|
process.once("exit", () => {
|
|
391
44
|
applyTerminalReset();
|
|
@@ -398,7 +51,7 @@ const runUi = () => {
|
|
|
398
51
|
render(React.createElement(App));
|
|
399
52
|
};
|
|
400
53
|
|
|
401
|
-
const runCommand = ()
|
|
54
|
+
const runCommand = async () => {
|
|
402
55
|
const args = process.argv.slice(2);
|
|
403
56
|
if (args.length === 0) {
|
|
404
57
|
runUi();
|
|
@@ -415,31 +68,57 @@ const runCommand = (): void => {
|
|
|
415
68
|
switch (primary) {
|
|
416
69
|
case "changes":
|
|
417
70
|
if (secondary === "create") {
|
|
418
|
-
runChangesCreate(rest);
|
|
71
|
+
runChangesCreate(rest, getStartDir());
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
if (secondary === "status") {
|
|
75
|
+
runChangesStatus(rest, getStartDir());
|
|
419
76
|
return;
|
|
420
77
|
}
|
|
421
78
|
break;
|
|
422
79
|
case "project":
|
|
423
80
|
if (secondary === "create") {
|
|
424
|
-
runProjectCreate(rest);
|
|
81
|
+
runProjectCreate(rest, getStartDir());
|
|
425
82
|
return;
|
|
426
83
|
}
|
|
427
84
|
break;
|
|
428
85
|
case "tasks":
|
|
429
86
|
if (secondary === "list") {
|
|
430
|
-
runTasksList(rest);
|
|
87
|
+
runTasksList(resolveSchubDir(), rest);
|
|
431
88
|
return;
|
|
432
89
|
}
|
|
433
90
|
if (secondary === "create") {
|
|
434
|
-
runTasksCreate(rest);
|
|
91
|
+
runTasksCreate(rest, getStartDir());
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
break;
|
|
95
|
+
case "review":
|
|
96
|
+
if (secondary === "create") {
|
|
97
|
+
runReviewCreate(rest, getStartDir());
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
if (secondary === "complete") {
|
|
101
|
+
runReviewComplete(rest, getStartDir());
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
break;
|
|
105
|
+
case "adr":
|
|
106
|
+
if (secondary === "create") {
|
|
107
|
+
runAdrCreate(rest, getStartDir());
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
break;
|
|
111
|
+
case "cookbook":
|
|
112
|
+
if (secondary === "create") {
|
|
113
|
+
runCookbookCreate(rest, getStartDir());
|
|
435
114
|
return;
|
|
436
115
|
}
|
|
437
116
|
break;
|
|
438
|
-
case "
|
|
439
|
-
|
|
117
|
+
case "eject":
|
|
118
|
+
runEject(args.slice(1), getStartDir());
|
|
440
119
|
return;
|
|
441
|
-
case "
|
|
442
|
-
|
|
120
|
+
case "init":
|
|
121
|
+
await runInit(args.slice(1), getStartDir());
|
|
443
122
|
return;
|
|
444
123
|
case "ui":
|
|
445
124
|
runUi();
|
|
@@ -449,10 +128,8 @@ const runCommand = (): void => {
|
|
|
449
128
|
printHelp(1);
|
|
450
129
|
};
|
|
451
130
|
|
|
452
|
-
|
|
453
|
-
runCommand();
|
|
454
|
-
} catch (error) {
|
|
131
|
+
runCommand().catch((error) => {
|
|
455
132
|
const message = error instanceof Error ? error.message : String(error);
|
|
456
133
|
process.stderr.write(`${message}\n`);
|
|
457
134
|
process.exitCode = 1;
|
|
458
|
-
}
|
|
135
|
+
});
|
package/src/init.test.ts
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { expect, test } from "bun:test";
|
|
2
|
+
import { spawnSync } from "node:child_process";
|
|
3
|
+
import { existsSync, mkdirSync, mkdtempSync, realpathSync, statSync } from "node:fs";
|
|
4
|
+
import { tmpdir } from "node:os";
|
|
5
|
+
import { join } from "node:path";
|
|
6
|
+
import { initSchubRoot } from "./init";
|
|
7
|
+
|
|
8
|
+
const createRepoFixture = () => {
|
|
9
|
+
const base = mkdtempSync(join(tmpdir(), "schub-init-root-"));
|
|
10
|
+
const repoRoot = join(base, "repo");
|
|
11
|
+
const startDir = join(repoRoot, "nested", "dir");
|
|
12
|
+
mkdirSync(startDir, { recursive: true });
|
|
13
|
+
return { base, repoRoot, startDir };
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
test("initSchubRoot resolves git worktree root and creates .schub", () => {
|
|
17
|
+
const { repoRoot, startDir } = createRepoFixture();
|
|
18
|
+
const result = spawnSync("git", ["init"], { cwd: repoRoot, stdio: "ignore" });
|
|
19
|
+
|
|
20
|
+
expect(result.status).toBe(0);
|
|
21
|
+
|
|
22
|
+
const schubRoot = initSchubRoot(startDir);
|
|
23
|
+
|
|
24
|
+
expect(realpathSync(schubRoot)).toBe(realpathSync(join(repoRoot, ".schub")));
|
|
25
|
+
expect(existsSync(schubRoot)).toBe(true);
|
|
26
|
+
expect(statSync(schubRoot).isDirectory()).toBe(true);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
test("initSchubRoot falls back to startDir when git is unavailable", () => {
|
|
30
|
+
const { startDir } = createRepoFixture();
|
|
31
|
+
const originalPath = process.env.PATH;
|
|
32
|
+
process.env.PATH = "";
|
|
33
|
+
|
|
34
|
+
try {
|
|
35
|
+
const schubRoot = initSchubRoot(startDir);
|
|
36
|
+
|
|
37
|
+
expect(schubRoot).toBe(join(startDir, ".schub"));
|
|
38
|
+
expect(existsSync(schubRoot)).toBe(true);
|
|
39
|
+
expect(statSync(schubRoot).isDirectory()).toBe(true);
|
|
40
|
+
} finally {
|
|
41
|
+
process.env.PATH = originalPath;
|
|
42
|
+
}
|
|
43
|
+
});
|
package/src/init.ts
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
import { mkdirSync } from "node:fs";
|
|
3
|
+
import { join, resolve } from "node:path";
|
|
4
|
+
|
|
5
|
+
export const resolveGitRoot = (startDir: string) => {
|
|
6
|
+
const result = spawnSync("git", ["rev-parse", "--show-toplevel"], {
|
|
7
|
+
cwd: startDir,
|
|
8
|
+
encoding: "utf8",
|
|
9
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
if (result.status !== 0) {
|
|
13
|
+
return null;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const output = result.stdout?.trim();
|
|
17
|
+
return output ? resolve(output) : null;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export const initSchubRoot = (startDir: string = process.cwd()) => {
|
|
21
|
+
const resolvedStart = resolve(startDir);
|
|
22
|
+
const gitRoot = resolveGitRoot(resolvedStart);
|
|
23
|
+
const root = gitRoot ?? resolvedStart;
|
|
24
|
+
const schubRoot = join(root, ".schub");
|
|
25
|
+
mkdirSync(schubRoot, { recursive: true });
|
|
26
|
+
return schubRoot;
|
|
27
|
+
};
|
package/src/project.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
|
-
import { existsSync, mkdirSync, readFileSync,
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
2
|
import { basename, dirname, join, resolve } from "node:path";
|
|
3
3
|
import { fileURLToPath } from "node:url";
|
|
4
|
+
import { resolveSchubRoot } from "./schub-root";
|
|
5
|
+
import { resolveTemplatePath } from "./templates";
|
|
4
6
|
|
|
5
7
|
type ProjectCreateOptions = {
|
|
6
8
|
repoRoot?: string;
|
|
@@ -16,36 +18,6 @@ const TEMPLATE_FILES = {
|
|
|
16
18
|
|
|
17
19
|
const TEMPLATES_ROOT = fileURLToPath(new URL("../templates/setup-project", import.meta.url));
|
|
18
20
|
|
|
19
|
-
const isDirectory = (path: string) => {
|
|
20
|
-
try {
|
|
21
|
-
return statSync(path).isDirectory();
|
|
22
|
-
} catch {
|
|
23
|
-
return false;
|
|
24
|
-
}
|
|
25
|
-
};
|
|
26
|
-
|
|
27
|
-
const resolveSchubRoot = (startDir: string) => {
|
|
28
|
-
let current = resolve(startDir);
|
|
29
|
-
const fallback = join(current, ".schub");
|
|
30
|
-
|
|
31
|
-
while (true) {
|
|
32
|
-
if (basename(current) === ".schub" && isDirectory(current)) {
|
|
33
|
-
return current;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
const candidate = join(current, ".schub");
|
|
37
|
-
if (isDirectory(candidate)) {
|
|
38
|
-
return candidate;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
const parent = dirname(current);
|
|
42
|
-
if (parent === current) {
|
|
43
|
-
return fallback;
|
|
44
|
-
}
|
|
45
|
-
current = parent;
|
|
46
|
-
}
|
|
47
|
-
};
|
|
48
|
-
|
|
49
21
|
const readTemplate = (path: string) => {
|
|
50
22
|
try {
|
|
51
23
|
return readFileSync(path, "utf8");
|
|
@@ -84,7 +56,8 @@ export const createProject = (startDir: string, options: ProjectCreateOptions) =
|
|
|
84
56
|
const created: string[] = [];
|
|
85
57
|
|
|
86
58
|
for (const [outputName, templateName] of Object.entries(TEMPLATE_FILES)) {
|
|
87
|
-
const
|
|
59
|
+
const bundledPath = join(TEMPLATES_ROOT, templateName);
|
|
60
|
+
const templatePath = resolveTemplatePath(schubRoot, join("setup-project", templateName), bundledPath);
|
|
88
61
|
const template = readTemplate(templatePath);
|
|
89
62
|
const rendered = template.split("[Project Name]").join(projectName);
|
|
90
63
|
const outputPath = join(schubRoot, outputName);
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { statSync } from "node:fs";
|
|
2
|
+
import { basename, dirname, join, resolve } from "node:path";
|
|
3
|
+
|
|
4
|
+
const isDirectory = (path: string) => {
|
|
5
|
+
try {
|
|
6
|
+
return statSync(path).isDirectory();
|
|
7
|
+
} catch {
|
|
8
|
+
return false;
|
|
9
|
+
}
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export const resolveSchubRoot = (startDir: string = process.cwd()) => {
|
|
13
|
+
const start = resolve(startDir);
|
|
14
|
+
const fallback = join(start, ".schub");
|
|
15
|
+
let current = start;
|
|
16
|
+
|
|
17
|
+
while (true) {
|
|
18
|
+
if (basename(current) === ".schub" && isDirectory(current)) {
|
|
19
|
+
return current;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const candidate = join(current, ".schub");
|
|
23
|
+
if (isDirectory(candidate)) {
|
|
24
|
+
return candidate;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const parent = dirname(current);
|
|
28
|
+
if (parent === current) {
|
|
29
|
+
return fallback;
|
|
30
|
+
}
|
|
31
|
+
current = parent;
|
|
32
|
+
}
|
|
33
|
+
};
|
package/src/templates.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { statSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
|
|
4
|
+
const isFile = (path: string) => {
|
|
5
|
+
try {
|
|
6
|
+
return statSync(path).isFile();
|
|
7
|
+
} catch {
|
|
8
|
+
return false;
|
|
9
|
+
}
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export const resolveTemplatePath = (schubDir: string, templatePath: string, bundledPath: string) => {
|
|
13
|
+
const localPath = join(schubDir, "templates", templatePath);
|
|
14
|
+
if (isFile(localPath)) {
|
|
15
|
+
return localPath;
|
|
16
|
+
}
|
|
17
|
+
return bundledPath;
|
|
18
|
+
};
|