flanders 0.6.1 → 0.8.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.
@@ -1,11 +1,13 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.SEPARATOR_GLYPH = exports.RESET = exports.ORANGE = exports.DIM = exports.BLUE = exports.GREEN = exports.MAGENTA = exports.YELLOW = exports.CYAN = void 0;
3
+ exports.SEPARATOR_GLYPH = exports.RESET = exports.ORANGE = exports.DIM = exports.BLUE = exports.RED = exports.GREEN = exports.MAGENTA = exports.YELLOW = exports.CYAN = void 0;
4
4
  exports.colorize = colorize;
5
5
  exports.stripAnsi = stripAnsi;
6
6
  exports.renderSegments = renderSegments;
7
+ exports.segmentsWidth = segmentsWidth;
7
8
  exports.renderSegmentsToWidth = renderSegmentsToWidth;
8
9
  exports.formatCountdown = formatCountdown;
10
+ exports.formatCompactCountdown = formatCompactCountdown;
9
11
  exports.formatDateTime = formatDateTime;
10
12
  exports.truncateToWidth = truncateToWidth;
11
13
  exports.formatTokens = formatTokens;
@@ -17,11 +19,13 @@ exports.formatSnapshotMetrics = formatSnapshotMetrics;
17
19
  exports.formatSnapshotBlock = formatSnapshotBlock;
18
20
  exports.formatReviewingFooter = formatReviewingFooter;
19
21
  exports.formatWorkingFooter = formatWorkingFooter;
22
+ exports.formatTerminalFooter = formatTerminalFooter;
20
23
  exports.formatWaitingFooter = formatWaitingFooter;
21
24
  exports.CYAN = "\x1b[36m";
22
25
  exports.YELLOW = "\x1b[33m";
23
26
  exports.MAGENTA = "\x1b[35m";
24
27
  exports.GREEN = "\x1b[32m";
28
+ exports.RED = "\x1b[31m";
25
29
  exports.BLUE = "\x1b[34m";
26
30
  exports.DIM = "\x1b[2m";
27
31
  exports.ORANGE = "\x1b[38;5;208m";
@@ -45,11 +49,14 @@ function renderSegments(segments) {
45
49
  }
46
50
  return result;
47
51
  }
48
- function renderSegmentsToWidth(segments, cols) {
49
- let plainLen = 0;
52
+ function segmentsWidth(segments) {
53
+ let width = 0;
50
54
  for (const seg of segments)
51
- plainLen += seg.text.length;
52
- if (plainLen <= cols)
55
+ width += seg.text.length;
56
+ return width;
57
+ }
58
+ function renderSegmentsToWidth(segments, cols) {
59
+ if (segmentsWidth(segments) <= cols)
53
60
  return renderSegments(segments);
54
61
  let result = "";
55
62
  let remaining = Math.max(0, cols - 1);
@@ -68,21 +75,41 @@ function renderSegmentsToWidth(segments, cols) {
68
75
  result += "…";
69
76
  return result;
70
77
  }
71
- function formatCountdown(remainingMs) {
78
+ function decomposeCountdown(remainingMs) {
72
79
  const totalMinutes = Math.max(1, Math.ceil(remainingMs / 60000));
73
80
  if (totalMinutes >= 24 * 60) {
74
81
  const days = Math.floor(totalMinutes / (24 * 60));
75
82
  const remainder = totalMinutes - days * 24 * 60;
76
83
  const hours = Math.floor(remainder / 60);
77
84
  const minutes = remainder % 60;
78
- return `${days} days, ${hours} hours, ${minutes} minutes`;
85
+ return { tier: "days", days, hours, minutes };
79
86
  }
80
87
  if (totalMinutes >= 60) {
81
88
  const hours = Math.floor(totalMinutes / 60);
82
89
  const minutes = totalMinutes % 60;
90
+ return { tier: "hours", days: 0, hours, minutes };
91
+ }
92
+ return { tier: "minutes", days: 0, hours: 0, minutes: totalMinutes };
93
+ }
94
+ function formatCountdown(remainingMs) {
95
+ const { tier, days, hours, minutes } = decomposeCountdown(remainingMs);
96
+ if (tier === "days") {
97
+ return `${days} days, ${hours} hours, ${minutes} minutes`;
98
+ }
99
+ if (tier === "hours") {
83
100
  return `${hours} hours ${minutes} minutes`;
84
101
  }
85
- return `${totalMinutes} minutes`;
102
+ return `${minutes} minutes`;
103
+ }
104
+ function formatCompactCountdown(remainingMs) {
105
+ const { tier, days, hours, minutes } = decomposeCountdown(remainingMs);
106
+ if (tier === "days") {
107
+ return `${days}d${hours}h${minutes}m`;
108
+ }
109
+ if (tier === "hours") {
110
+ return `${hours}h${minutes}m`;
111
+ }
112
+ return `${minutes}m`;
86
113
  }
87
114
  function formatDateTime(date) {
88
115
  const pad = (n) => String(n).padStart(2, "0");
@@ -192,10 +219,7 @@ function formatMetricsLine(task, plan, cols) {
192
219
  fullSegments.push({ text: " " }, { text: "│", color: exports.DIM }, { text: " " });
193
220
  if (plan)
194
221
  fullSegments.push(...buildPairFullSegments("plan", pTok, pTime));
195
- let fullPlainLen = 0;
196
- for (const seg of fullSegments)
197
- fullPlainLen += seg.text.length;
198
- if (fullPlainLen <= cols)
222
+ if (segmentsWidth(fullSegments) <= cols)
199
223
  return renderSegments(fullSegments);
200
224
  const compactSegments = [];
201
225
  if (task)
@@ -204,10 +228,7 @@ function formatMetricsLine(task, plan, cols) {
204
228
  compactSegments.push({ text: "│", color: exports.DIM });
205
229
  if (plan)
206
230
  compactSegments.push(...buildPairCompactSegments("p:", pTok, pTime));
207
- let compactPlainLen = 0;
208
- for (const seg of compactSegments)
209
- compactPlainLen += seg.text.length;
210
- if (compactPlainLen <= cols)
231
+ if (segmentsWidth(compactSegments) <= cols)
211
232
  return renderSegments(compactSegments);
212
233
  return renderSegmentsToWidth(compactSegments, cols);
213
234
  }
@@ -235,36 +256,40 @@ function reviewerEntryDescriptor(model, effort) {
235
256
  const effortToken = effort === "" ? "default" : effort;
236
257
  return `(${modelToken} ${effortToken})`;
237
258
  }
238
- function buildReviewingFullText(reviewers) {
239
- let line = "review: ";
240
- for (let i = 0; i < reviewers.length; i++) {
241
- const r = reviewers[i];
242
- if (i > 0)
243
- line += ", ";
244
- line += `${r.tool} ${reviewerEntryDescriptor(r.model, r.effort)}: ${r.state}`;
259
+ function reviewerStateText(r, nowMs) {
260
+ if (r.state === "waiting" && r.endTime != null) {
261
+ return `waiting ${formatCompactCountdown(Math.max(0, r.endTime - nowMs))}`;
245
262
  }
246
- return line;
263
+ return r.state;
247
264
  }
248
- function buildReviewingCompactText(reviewers) {
249
- let line = "review: ";
265
+ function reviewerEntryColor(state) {
266
+ if (state === "pass")
267
+ return exports.GREEN;
268
+ if (state === "fail")
269
+ return exports.RED;
270
+ return exports.ORANGE;
271
+ }
272
+ function buildReviewingSegments(frame, reviewers, nowMs, compact) {
273
+ const segments = [{ text: `${frame} review: `, color: exports.ORANGE }];
250
274
  for (let i = 0; i < reviewers.length; i++) {
251
275
  const r = reviewers[i];
252
276
  if (i > 0)
253
- line += ", ";
254
- line += `${r.tool}: ${r.state}`;
277
+ segments.push({ text: ", ", color: exports.ORANGE });
278
+ const descriptor = compact ? "" : ` ${reviewerEntryDescriptor(r.model, r.effort)}`;
279
+ segments.push({ text: `${r.tool}${descriptor}: ${reviewerStateText(r, nowMs)}`, color: reviewerEntryColor(r.state) });
255
280
  }
256
- return line;
281
+ return segments;
257
282
  }
258
- function formatReviewingFooter(reviewers, cols) {
259
- const fullText = buildReviewingFullText(reviewers);
260
- if (fullText.length <= cols) {
261
- return renderSegments([{ text: fullText, color: exports.ORANGE }]);
283
+ function formatReviewingFooter(frame, reviewers, cols, nowMs) {
284
+ const full = buildReviewingSegments(frame, reviewers, nowMs, false);
285
+ if (segmentsWidth(full) <= cols) {
286
+ return renderSegments(full);
262
287
  }
263
- const compactText = buildReviewingCompactText(reviewers);
264
- if (compactText.length <= cols) {
265
- return renderSegments([{ text: compactText, color: exports.ORANGE }]);
288
+ const compact = buildReviewingSegments(frame, reviewers, nowMs, true);
289
+ if (segmentsWidth(compact) <= cols) {
290
+ return renderSegments(compact);
266
291
  }
267
- return renderSegmentsToWidth([{ text: compactText, color: exports.ORANGE }], cols);
292
+ return renderSegmentsToWidth(compact, cols);
268
293
  }
269
294
  function fitOrangeFooterLine(text, cols) {
270
295
  if (text.length <= cols) {
@@ -272,8 +297,11 @@ function fitOrangeFooterLine(text, cols) {
272
297
  }
273
298
  return renderSegmentsToWidth([{ text, color: exports.ORANGE }], cols);
274
299
  }
275
- function formatWorkingFooter(frame, cols) {
276
- return fitOrangeFooterLine(`${frame} Working`, cols);
300
+ function formatWorkingFooter(frame, label, cols) {
301
+ return fitOrangeFooterLine(`${frame} ${label}`, cols);
302
+ }
303
+ function formatTerminalFooter(label, cols) {
304
+ return fitOrangeFooterLine(label, cols);
277
305
  }
278
306
  function formatWaitingFooter(heading, dateTime, countdown, cols) {
279
307
  return fitOrangeFooterLine(`${heading} — ${dateTime} — ${countdown}`, cols);
@@ -0,0 +1,15 @@
1
+ import type { RandomContext } from "./contexts";
2
+ export declare const workingPool: readonly string[];
3
+ export declare const successPool: readonly string[];
4
+ export declare const hardStopPool: readonly string[];
5
+ export declare const interruptionPool: readonly string[];
6
+ export declare const failurePool: readonly string[];
7
+ export declare const tasksCompletedPool: readonly string[];
8
+ export declare const allTasksCompletedPool: readonly string[];
9
+ export declare const terminalPools: {
10
+ Done: readonly string[];
11
+ "Hard stop": readonly string[];
12
+ Interrupted: readonly string[];
13
+ Failed: readonly string[];
14
+ };
15
+ export declare function pickVariant(pool: readonly string[], random: RandomContext, exclude?: string): string;
@@ -0,0 +1,141 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.terminalPools = exports.allTasksCompletedPool = exports.tasksCompletedPool = exports.failurePool = exports.interruptionPool = exports.hardStopPool = exports.successPool = exports.workingPool = void 0;
4
+ exports.pickVariant = pickVariant;
5
+ exports.workingPool = [
6
+ "Workin'-diddly",
7
+ "Toilin' away",
8
+ "Okely-workin'",
9
+ "Pluggin' away",
10
+ "Diddly-doin'",
11
+ "Beaverin' away",
12
+ "Tinkerin'-diddly",
13
+ "Hammerin' away",
14
+ "Whittlin' away",
15
+ "Doodly-doin'",
16
+ "Scribblin'-diddly",
17
+ "Codin'-aroo",
18
+ "Buildin'-diddly",
19
+ "Fixin'-diddly",
20
+ "Noodlin' away",
21
+ "Tappity-tappin'",
22
+ "Diddly-dabblin'",
23
+ "Plowin' ahead",
24
+ "Chuggin' along",
25
+ "Pressin' on-aroo",
26
+ "Diligent-diddly",
27
+ "Steady-diddly",
28
+ "Crunchin'-diddly",
29
+ "Diddly-developin'",
30
+ "Wranglin' code",
31
+ "Pokin' at it",
32
+ "Diddly-debuggin'",
33
+ "Cookin'-diddly",
34
+ "Trundlin' along",
35
+ "Diddly-draftin'",
36
+ "Bustlin'-aroo",
37
+ "Hummin' along",
38
+ "Choppin'-diddly",
39
+ "Sweatin'-diddly",
40
+ "Diddly-deliverin'",
41
+ "Craftin'-aroo",
42
+ "Workin'-aroo",
43
+ "Toilin'-diddly",
44
+ "Diddly-drudgin'",
45
+ "Gettin' it done",
46
+ "Grindin'-diddly",
47
+ "Diddly-graftin'",
48
+ "Peggin' away",
49
+ "Diddly-doodlin'",
50
+ "Crankin'-diddly",
51
+ "Hustlin'-diddly",
52
+ "Diddly-diggin'",
53
+ "Pluggin'-diddly",
54
+ "Tappin' keys",
55
+ "Dilly-workin'",
56
+ ];
57
+ exports.successPool = [
58
+ "Done-diddly",
59
+ "Done-diddly-done",
60
+ "All wrapped up, neighbor",
61
+ "Okely-dokely — done",
62
+ "Done-aroo",
63
+ "Hi-diddly-done",
64
+ "All done, neighborino",
65
+ "Done and done-diddly",
66
+ "That's a wrap, neighbor",
67
+ "Mission accomplished-diddly",
68
+ ];
69
+ exports.hardStopPool = [
70
+ "Whoopsie, hard stop",
71
+ "Dilly of a pickle — hard stop",
72
+ "Hard stop, neighbor",
73
+ "Hard stop-aroo",
74
+ "Well, heck — hard stop",
75
+ "Hard stop-diddly",
76
+ "Fiddlesticks — hard stop",
77
+ "Gotta call it — hard stop",
78
+ "Pumpin' the brakes — hard stop",
79
+ "Hard stop, neighborino",
80
+ ];
81
+ exports.interruptionPool = [
82
+ "Interrupted-aroo",
83
+ "Well, heck — interrupted",
84
+ "Interrupted, neighbor",
85
+ "Stopped short-diddly",
86
+ "Cut off-aroo",
87
+ "Hold the phone — interrupted",
88
+ "Interrupted-diddly",
89
+ "Toodle-oo — interrupted",
90
+ "Interrupted, neighborino",
91
+ "Halted-aroo",
92
+ ];
93
+ exports.failurePool = [
94
+ "Aw, fiddlesticks — failed",
95
+ "Dad-gummit — failed",
96
+ "Failed, neighbor",
97
+ "Failed-aroo",
98
+ "Gosh-darn it — failed",
99
+ "Heavens to Betsy — failed",
100
+ "Failed-diddly",
101
+ "Well, shucks — failed",
102
+ "That's a no-go — failed",
103
+ "Failed, neighborino",
104
+ ];
105
+ exports.tasksCompletedPool = [
106
+ "tasks completed — nothin' to do-diddly-do, neighbor!",
107
+ "all done already, neighbor — okely-dokely!",
108
+ "nothin' left to do here, neighbor!",
109
+ "tasks completed — already shipshape, neighborino!",
110
+ "nothin' to do-diddly-do — all set, neighbor!",
111
+ "tasks completed — couldn't be tidier, neighbor!",
112
+ "all squared away already — okely-dokely!",
113
+ "nothin' doin' here — all done, neighbor!",
114
+ "tasks completed — easy-peasy, neighborino!",
115
+ "all wrapped up already, neighbor — toodle-oo!",
116
+ ];
117
+ exports.allTasksCompletedPool = [
118
+ "all tasks completed — okely-dokely-doo, neighbor!",
119
+ "all tasks completed — hi-diddly-done, neighbor!",
120
+ "every last task done-diddly-done, neighbor!",
121
+ "all tasks completed — that's a wrap, neighborino!",
122
+ "all tasks completed — mission accomplished-diddly!",
123
+ "all tasks done and dusted, neighbor — okely-dokely!",
124
+ "all tasks completed — nailed it-aroo, neighbor!",
125
+ "every task done-diddly-done — toodle-oo, neighbor!",
126
+ "all tasks completed — what a humdinger, neighbor!",
127
+ "all tasks completed — done and done, neighborino!",
128
+ ];
129
+ exports.terminalPools = {
130
+ "Done": exports.successPool,
131
+ "Hard stop": exports.hardStopPool,
132
+ "Interrupted": exports.interruptionPool,
133
+ "Failed": exports.failurePool,
134
+ };
135
+ function pickVariant(pool, random, exclude) {
136
+ const candidates = exclude !== undefined && pool.length > 1
137
+ ? pool.filter(variant => variant !== exclude)
138
+ : pool;
139
+ const index = Math.floor(random.random() * candidates.length);
140
+ return candidates[index];
141
+ }
@@ -16,12 +16,13 @@ type ReadArgs = Readonly<{
16
16
  projectRoot: string;
17
17
  homeDir: string;
18
18
  }>;
19
- type WriteArgs = Readonly<{
19
+ type ScopedConfigArgs = ReadArgs & Readonly<{
20
20
  scope: "project" | "global";
21
- projectRoot: string;
22
- homeDir: string;
21
+ }>;
22
+ type WriteArgs = ScopedConfigArgs & Readonly<{
23
23
  config: FlandersConfig;
24
24
  }>;
25
25
  export declare function read(fs: FsContext, args: ReadArgs): Promise<FlandersConfig | null>;
26
+ export declare function readScope(fs: FsContext, args: ScopedConfigArgs): Promise<FlandersConfig | null>;
26
27
  export declare function write(fs: FsContext, args: WriteArgs): Promise<string>;
27
28
  export {};
@@ -1,10 +1,12 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.read = read;
4
+ exports.readScope = readScope;
4
5
  exports.write = write;
5
6
  const fsUtils_1 = require("../system/fsUtils");
6
7
  const CONFIG_PATH = ".flanders/config.json";
7
8
  const CONFIG_DIR = ".flanders";
9
+ const ALLOWED_TOP_LEVEL_KEYS = ["worker", "reviewers", "minimumReviews"];
8
10
  function configPath(base) {
9
11
  return (0, fsUtils_1.joinPath)(base, CONFIG_PATH);
10
12
  }
@@ -42,6 +44,11 @@ function validate(raw, filePath) {
42
44
  if (typeof minimumReviews !== "number" || !Number.isInteger(minimumReviews) || minimumReviews < 1 || minimumReviews > reviewers.length) {
43
45
  throw new Error(`Malformed config at ${filePath}: field "minimumReviews" must be an integer in [1, ${reviewers.length}]`);
44
46
  }
47
+ for (const key of Object.keys(obj)) {
48
+ if (!ALLOWED_TOP_LEVEL_KEYS.includes(key)) {
49
+ throw new Error(`Malformed config at ${filePath}: unexpected top-level key "${key}"`);
50
+ }
51
+ }
45
52
  return raw;
46
53
  }
47
54
  function validateRole(role, name, filePath) {
@@ -85,6 +92,17 @@ async function read(fs, args) {
85
92
  }
86
93
  return null;
87
94
  }
95
+ async function readScope(fs, args) {
96
+ const base = args.scope === "project" ? args.projectRoot : args.homeDir;
97
+ const target = configPath(base);
98
+ try {
99
+ const content = await fs.readFile(target);
100
+ return validate(JSON.parse(content), target);
101
+ }
102
+ catch {
103
+ return null;
104
+ }
105
+ }
88
106
  async function write(fs, args) {
89
107
  const base = args.scope === "project" ? args.projectRoot : args.homeDir;
90
108
  const dir = configDir(base);
@@ -4,6 +4,7 @@ export type WorkspacePaths = Readonly<{
4
4
  buildScript: string;
5
5
  testScript: string;
6
6
  errorLog: string;
7
+ specFile: string;
7
8
  prepLog(taskIndex: number): string;
8
9
  workerLog(iter: number): string;
9
10
  buildLog(iter: number): string;
@@ -11,6 +12,7 @@ export type WorkspacePaths = Readonly<{
11
12
  reviewerLog(iter: number): string;
12
13
  reviewerOutputLog(iter: number, reviewerIndex: number): string;
13
14
  reviewerErrorLog(reviewerIndex: number): string;
15
+ reviewerSpecFile(reviewerIndex: number): string;
14
16
  }>;
15
17
  export interface PlatformContext {
16
18
  isWindows(): boolean;
@@ -44,13 +44,15 @@ class Workspace {
44
44
  buildScript: (0, fsUtils_1.joinPath)(root, isWindows ? "build.bat" : "build.sh"),
45
45
  testScript: (0, fsUtils_1.joinPath)(root, isWindows ? "test.bat" : "test.sh"),
46
46
  errorLog: (0, fsUtils_1.joinPath)(root, "error.log"),
47
+ specFile: (0, fsUtils_1.joinPath)(root, "spec.md"),
47
48
  prepLog(taskIndex) { return (0, fsUtils_1.joinPath)(root, `prep.${taskIndex}.log`); },
48
49
  workerLog(iter) { return (0, fsUtils_1.joinPath)(root, `worker.${iter}.log`); },
49
50
  buildLog(iter) { return (0, fsUtils_1.joinPath)(root, `build.${iter}.log`); },
50
51
  testLog(iter) { return (0, fsUtils_1.joinPath)(root, `test.${iter}.log`); },
51
52
  reviewerLog(iter) { return (0, fsUtils_1.joinPath)(root, `reviewer.${iter}.log`); },
52
53
  reviewerOutputLog(iter, reviewerIndex) { return (0, fsUtils_1.joinPath)(root, `reviewer.${iter}.${reviewerIndex}.log`); },
53
- reviewerErrorLog(reviewerIndex) { return (0, fsUtils_1.joinPath)(reviewerRoots[reviewerIndex - 1], "error.log"); }
54
+ reviewerErrorLog(reviewerIndex) { return (0, fsUtils_1.joinPath)(reviewerRoots[reviewerIndex - 1], "error.log"); },
55
+ reviewerSpecFile(reviewerIndex) { return (0, fsUtils_1.joinPath)(reviewerRoots[reviewerIndex - 1], "spec.md"); }
54
56
  };
55
57
  }
56
58
  async errorLogExists() {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "flanders",
3
- "version": "0.6.1",
3
+ "version": "0.8.0",
4
4
  "description": "Flanders never breaks a rule",
5
5
  "main": "lib/",
6
6
  "types": "lib/types",