schub 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/dist/index.js +36665 -0
- package/package.json +32 -0
- package/src/App.tsx +103 -0
- package/src/changes.ts +237 -0
- package/src/components/PlanView.tsx +91 -0
- package/src/components/StatusView.tsx +217 -0
- package/src/components/statusColor.ts +1 -0
- package/src/features/tasks/constants.ts +30 -0
- package/src/features/tasks/create.ts +122 -0
- package/src/features/tasks/filesystem.ts +178 -0
- package/src/features/tasks/graph.ts +87 -0
- package/src/features/tasks/index.ts +12 -0
- package/src/features/tasks/sorting.ts +14 -0
- package/src/index.ts +458 -0
- package/src/project.ts +96 -0
- package/src/tasks.ts +15 -0
- package/src/terminal.ts +16 -0
- package/templates/create-proposal/adr-template.md +51 -0
- package/templates/create-proposal/proposal-template.md +51 -0
- package/templates/create-tasks/task-template.md +33 -0
- package/templates/review-proposal/q&a-template.md +13 -0
- package/templates/review-proposal/review-me-template.md +18 -0
- package/templates/setup-project/project-overview-template.md +35 -0
- package/templates/setup-project/project-setup-template.md +61 -0
- package/templates/setup-project/project-wow-template.md +130 -0
- package/templates/setup-project/review-me-template.md +18 -0
package/src/index.ts
ADDED
|
@@ -0,0 +1,458 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
import { dirname } from "node:path";
|
|
3
|
+
import { spawnSync } from "bun";
|
|
4
|
+
import { render } from "ink";
|
|
5
|
+
import React from "react";
|
|
6
|
+
import App from "./App";
|
|
7
|
+
import { createChange, resolveChangeRoot } from "./changes";
|
|
8
|
+
import { createTask, findSchubRoot, listTasks, TASK_STATUSES, type TaskStatus } from "./features/tasks";
|
|
9
|
+
import { createProject } from "./project";
|
|
10
|
+
import { applyTerminalPrelude, applyTerminalReset } from "./terminal";
|
|
11
|
+
|
|
12
|
+
const HELP_TEXT = `schub [command]
|
|
13
|
+
|
|
14
|
+
Commands:
|
|
15
|
+
changes create Create a change proposal
|
|
16
|
+
project create Create project docs
|
|
17
|
+
tasks create Create task files for a change
|
|
18
|
+
tasks list List tasks
|
|
19
|
+
lint Run Biome lint checks
|
|
20
|
+
format Format files with Biome
|
|
21
|
+
ui Launch the interactive dashboard
|
|
22
|
+
`;
|
|
23
|
+
|
|
24
|
+
const getStartDir = (): string => process.env.SCHUB_CWD ?? process.cwd();
|
|
25
|
+
|
|
26
|
+
const getWorkspaceRoot = (): string => {
|
|
27
|
+
const schubDir = findSchubRoot(getStartDir());
|
|
28
|
+
return schubDir ? dirname(schubDir) : getStartDir();
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const resolveSchubDir = (): string | null => findSchubRoot(getStartDir());
|
|
32
|
+
|
|
33
|
+
const printHelp = (exitCode = 0): void => {
|
|
34
|
+
process.stdout.write(`${HELP_TEXT}\n`);
|
|
35
|
+
process.exitCode = exitCode;
|
|
36
|
+
};
|
|
37
|
+
|
|
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
|
+
const registerTerminalReset = () => {
|
|
390
|
+
process.once("exit", () => {
|
|
391
|
+
applyTerminalReset();
|
|
392
|
+
});
|
|
393
|
+
};
|
|
394
|
+
|
|
395
|
+
const runUi = () => {
|
|
396
|
+
applyTerminalPrelude();
|
|
397
|
+
registerTerminalReset();
|
|
398
|
+
render(React.createElement(App));
|
|
399
|
+
};
|
|
400
|
+
|
|
401
|
+
const runCommand = (): void => {
|
|
402
|
+
const args = process.argv.slice(2);
|
|
403
|
+
if (args.length === 0) {
|
|
404
|
+
runUi();
|
|
405
|
+
return;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
if (args.includes("--help") || args.includes("-h")) {
|
|
409
|
+
printHelp();
|
|
410
|
+
return;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
const [primary, secondary, ...rest] = args;
|
|
414
|
+
|
|
415
|
+
switch (primary) {
|
|
416
|
+
case "changes":
|
|
417
|
+
if (secondary === "create") {
|
|
418
|
+
runChangesCreate(rest);
|
|
419
|
+
return;
|
|
420
|
+
}
|
|
421
|
+
break;
|
|
422
|
+
case "project":
|
|
423
|
+
if (secondary === "create") {
|
|
424
|
+
runProjectCreate(rest);
|
|
425
|
+
return;
|
|
426
|
+
}
|
|
427
|
+
break;
|
|
428
|
+
case "tasks":
|
|
429
|
+
if (secondary === "list") {
|
|
430
|
+
runTasksList(rest);
|
|
431
|
+
return;
|
|
432
|
+
}
|
|
433
|
+
if (secondary === "create") {
|
|
434
|
+
runTasksCreate(rest);
|
|
435
|
+
return;
|
|
436
|
+
}
|
|
437
|
+
break;
|
|
438
|
+
case "lint":
|
|
439
|
+
runBiome("lint");
|
|
440
|
+
return;
|
|
441
|
+
case "format":
|
|
442
|
+
runBiome("format");
|
|
443
|
+
return;
|
|
444
|
+
case "ui":
|
|
445
|
+
runUi();
|
|
446
|
+
return;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
printHelp(1);
|
|
450
|
+
};
|
|
451
|
+
|
|
452
|
+
try {
|
|
453
|
+
runCommand();
|
|
454
|
+
} catch (error) {
|
|
455
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
456
|
+
process.stderr.write(`${message}\n`);
|
|
457
|
+
process.exitCode = 1;
|
|
458
|
+
}
|
package/src/project.ts
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { basename, dirname, join, resolve } from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
|
|
5
|
+
type ProjectCreateOptions = {
|
|
6
|
+
repoRoot?: string;
|
|
7
|
+
projectName?: string;
|
|
8
|
+
overwrite?: boolean;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
const TEMPLATE_FILES = {
|
|
12
|
+
"project-overview.md": "project-overview-template.md",
|
|
13
|
+
"project-setup.md": "project-setup-template.md",
|
|
14
|
+
"project-wow.md": "project-wow-template.md",
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
const TEMPLATES_ROOT = fileURLToPath(new URL("../templates/setup-project", import.meta.url));
|
|
18
|
+
|
|
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
|
+
const readTemplate = (path: string) => {
|
|
50
|
+
try {
|
|
51
|
+
return readFileSync(path, "utf8");
|
|
52
|
+
} catch {
|
|
53
|
+
throw new Error(`[ERROR] Template not found: ${path}`);
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
const writeOutput = (path: string, content: string, overwrite: boolean) => {
|
|
58
|
+
if (existsSync(path) && !overwrite) {
|
|
59
|
+
throw new Error(`[ERROR] Refusing to overwrite existing file: ${path}`);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
63
|
+
writeFileSync(path, content, "utf8");
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
const deriveProjectName = (repoRoot: string) => {
|
|
67
|
+
const name = basename(repoRoot).trim();
|
|
68
|
+
return name || "Project";
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
const resolveRepoRoot = (startDir: string, schubRoot: string, repoRoot?: string) => {
|
|
72
|
+
if (repoRoot) {
|
|
73
|
+
return resolve(startDir, repoRoot);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return resolve(schubRoot, "..");
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
export const createProject = (startDir: string, options: ProjectCreateOptions) => {
|
|
80
|
+
const schubRoot = resolveSchubRoot(startDir);
|
|
81
|
+
const repoRoot = resolveRepoRoot(startDir, schubRoot, options.repoRoot);
|
|
82
|
+
const projectName = options.projectName || deriveProjectName(repoRoot);
|
|
83
|
+
const overwrite = options.overwrite ?? false;
|
|
84
|
+
const created: string[] = [];
|
|
85
|
+
|
|
86
|
+
for (const [outputName, templateName] of Object.entries(TEMPLATE_FILES)) {
|
|
87
|
+
const templatePath = join(TEMPLATES_ROOT, templateName);
|
|
88
|
+
const template = readTemplate(templatePath);
|
|
89
|
+
const rendered = template.split("[Project Name]").join(projectName);
|
|
90
|
+
const outputPath = join(schubRoot, outputName);
|
|
91
|
+
writeOutput(outputPath, rendered, overwrite);
|
|
92
|
+
created.push(outputPath);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return created;
|
|
96
|
+
};
|
package/src/tasks.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export {
|
|
2
|
+
buildTaskGraph,
|
|
3
|
+
findSchubRoot,
|
|
4
|
+
listTasks,
|
|
5
|
+
loadTaskDependencies,
|
|
6
|
+
renderTaskGraphLines,
|
|
7
|
+
TASK_STATUSES,
|
|
8
|
+
type TaskDependency,
|
|
9
|
+
type TaskGraph,
|
|
10
|
+
type TaskGraphLine,
|
|
11
|
+
type TaskGraphNode,
|
|
12
|
+
type TaskInfo,
|
|
13
|
+
type TaskStatus,
|
|
14
|
+
trimTaskTitle,
|
|
15
|
+
} from "./features/tasks";
|
package/src/terminal.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
const CLEAR_SCROLLBACK = "\u001b[3J";
|
|
2
|
+
const CLEAR_SCREEN = "\u001b[2J";
|
|
3
|
+
const CURSOR_HOME = "\u001b[H";
|
|
4
|
+
const BLACK_BACKGROUND = "\u001b[40m";
|
|
5
|
+
const RESET = "\u001b[0m";
|
|
6
|
+
|
|
7
|
+
export const terminalPrelude = `${CLEAR_SCROLLBACK}${CLEAR_SCREEN}${CURSOR_HOME}${BLACK_BACKGROUND}`;
|
|
8
|
+
export const terminalReset = RESET;
|
|
9
|
+
|
|
10
|
+
export const applyTerminalPrelude = () => {
|
|
11
|
+
process.stdout.write(terminalPrelude);
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export const applyTerminalReset = () => {
|
|
15
|
+
process.stdout.write(terminalReset);
|
|
16
|
+
};
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# ADR: [TITLE]
|
|
2
|
+
|
|
3
|
+
**Status**: [Proposed | Accepted | Deprecated | Superseded]
|
|
4
|
+
**Date**: [YYYY-MM-DD]
|
|
5
|
+
**Related**: [PR link, spec link, plan link, issue link]
|
|
6
|
+
|
|
7
|
+
> Use ADRs to capture the **WHY**, not the implementation details.
|
|
8
|
+
> Keep it short and specific: context → decision → rationale → consequences.
|
|
9
|
+
|
|
10
|
+
## Context
|
|
11
|
+
|
|
12
|
+
[What problem are we solving? What constraints, requirements, or incidents led here?]
|
|
13
|
+
|
|
14
|
+
## Decision
|
|
15
|
+
|
|
16
|
+
[What are we choosing to do? Be explicit and testable.]
|
|
17
|
+
|
|
18
|
+
## Rationale
|
|
19
|
+
|
|
20
|
+
[Why this decision? Include tradeoffs and decision drivers.]
|
|
21
|
+
|
|
22
|
+
## Alternatives Considered
|
|
23
|
+
|
|
24
|
+
1. **[Alternative A]**: [why rejected]
|
|
25
|
+
2. **[Alternative B]**: [why rejected]
|
|
26
|
+
|
|
27
|
+
## Consequences
|
|
28
|
+
|
|
29
|
+
### Positive
|
|
30
|
+
|
|
31
|
+
- [What gets better?]
|
|
32
|
+
|
|
33
|
+
### Negative / Risks
|
|
34
|
+
|
|
35
|
+
- [What gets worse or harder?]
|
|
36
|
+
|
|
37
|
+
## Migration / Rollout Plan
|
|
38
|
+
|
|
39
|
+
[How do we move from the current state to the decided state? Include any sequencing.]
|
|
40
|
+
|
|
41
|
+
## Verification & Evidence
|
|
42
|
+
|
|
43
|
+
_Goal: the decision’s impact is verifiable by an LLM through execution + concrete outputs._
|
|
44
|
+
|
|
45
|
+
- **Commands to run**: [exact commands]
|
|
46
|
+
- **Expected evidence**: [logs, HTTP responses, screenshots/videos/traces, artifacts]
|
|
47
|
+
- **Where to find artifacts**: [paths, URLs, container/log sources]
|
|
48
|
+
|
|
49
|
+
## Follow-ups
|
|
50
|
+
|
|
51
|
+
- [TODO items, owners, deadlines]
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# Proposal - {{CHANGE_TITLE}}
|
|
2
|
+
|
|
3
|
+
**Change ID**: `{{CHANGE_ID}}`
|
|
4
|
+
**Created**: {{DATE}}
|
|
5
|
+
**Status**: Draft
|
|
6
|
+
**Input**: {{INPUT}}
|
|
7
|
+
|
|
8
|
+
## Summary
|
|
9
|
+
|
|
10
|
+
[1–3 sentences describing the change and the user-visible outcome.]
|
|
11
|
+
|
|
12
|
+
## Why
|
|
13
|
+
|
|
14
|
+
[Problem/opportunity + motivation. What’s broken/missing today? Why now?]
|
|
15
|
+
|
|
16
|
+
## Goals
|
|
17
|
+
|
|
18
|
+
- [Outcome 1]
|
|
19
|
+
- [Outcome 2]
|
|
20
|
+
|
|
21
|
+
### Non-Goals
|
|
22
|
+
|
|
23
|
+
- [What is explicitly not included]
|
|
24
|
+
|
|
25
|
+
## User Scenarios & Acceptance
|
|
26
|
+
|
|
27
|
+
### Scenario 1 — [Title] (Priority: P1)
|
|
28
|
+
|
|
29
|
+
> **Given** [state], **When** [action], **Then** [outcome]
|
|
30
|
+
|
|
31
|
+
### Scenario 2 — [Title] (Priority: P2)
|
|
32
|
+
|
|
33
|
+
> **Given** [state], **When** [action], **Then** [outcome]
|
|
34
|
+
|
|
35
|
+
## Implementation Outline
|
|
36
|
+
|
|
37
|
+
1. [Phase 1]
|
|
38
|
+
2. [Phase 2]
|
|
39
|
+
|
|
40
|
+
## Change Touch Points
|
|
41
|
+
|
|
42
|
+
- [Exact paths, systems, or components touched]
|
|
43
|
+
|
|
44
|
+
## Assumptions
|
|
45
|
+
|
|
46
|
+
- A1: [Assumption]
|
|
47
|
+
- A2: [Assumption]
|
|
48
|
+
|
|
49
|
+
## Potential Issues
|
|
50
|
+
|
|
51
|
+
- [List potential issues, conflicts, discrepancies, risks]
|