@trieungoctam/speckit 0.3.5 → 0.3.6

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 CHANGED
@@ -30,6 +30,8 @@ npx @trieungoctam/speckit@latest session checkpoint --note "red complete"
30
30
  npx @trieungoctam/speckit@latest review
31
31
  ```
32
32
 
33
+ Interactive CLI output is colorized when the terminal supports ANSI colors. Use `NO_COLOR=1` to disable colors, or `SPECKIT_COLOR=1` to force colors for demos and snapshots.
34
+
33
35
  Or install globally:
34
36
 
35
37
  ```bash
package/dist/cli.js CHANGED
@@ -19,6 +19,7 @@ import { runCommand } from "./commands/run.js";
19
19
  import { readyCommand } from "./commands/ready.js";
20
20
  import { reviewCommand } from "./commands/review.js";
21
21
  import { closeCommand } from "./commands/close.js";
22
+ import { createColors } from "./core/colors.js";
22
23
  export async function main(argv = process.argv.slice(2), root = process.cwd()) {
23
24
  const parsed = parseArgs(argv);
24
25
  try {
@@ -145,7 +146,8 @@ function isHelp(parsed) {
145
146
  return parsed.command === "help" || parsed.command === "--help" || parsed.command === "-h" || has(parsed, "help");
146
147
  }
147
148
  function printHelp() {
148
- console.log(`Speckit - Agile + TDD workflow compiler for agentic IDEs
149
+ const colors = createColors();
150
+ console.log(`${colors.bold(colors.cyan("Speckit"))} - Agile + TDD workflow compiler for agentic IDEs
149
151
 
150
152
  Usage:
151
153
  speckit setup
@@ -6,8 +6,10 @@ import { detectTestCommands } from "../core/test-detection.js";
6
6
  import { coreFiles, enterpriseFiles } from "../core/scaffold.js";
7
7
  import { agentFiles } from "../core/agent-scaffold.js";
8
8
  import { validateWorkflowContract } from "../core/workflow-validator.js";
9
+ import { createColors } from "../core/colors.js";
9
10
  export async function doctorCommand(options) {
10
11
  const stdout = options.stdout ?? console;
12
+ const colors = createColors();
11
13
  const tools = checkTools();
12
14
  const tests = await detectTestCommands(options.root);
13
15
  const deepChecks = options.deep ? await runDeepChecks(options.root) : [];
@@ -29,24 +31,25 @@ export async function doctorCommand(options) {
29
31
  stdout.log(JSON.stringify(report, null, 2));
30
32
  }
31
33
  else {
32
- stdout.log(`Speckit doctor: ${report.status}`);
33
- stdout.log(`Node: ${report.node}`);
34
- stdout.log(`Tools: ${tools.map((tool) => `${tool.name}=${tool.available ? "ok" : "missing"}`).join(", ")}`);
34
+ stdout.log(`${colors.bold("Speckit doctor")}: ${colors.status(report.status === "ok", "ok", report.status)}`);
35
+ stdout.log(`${colors.bold("Node")}: ${report.node}`);
36
+ stdout.log(`Tools: ${tools.map((tool) => `${tool.name}=${colors.status(tool.available)}`).join(", ")}`);
35
37
  for (const tool of tools.filter((tool) => !tool.available)) {
36
- stdout.log(`Tool hint ${tool.name}: ${tool.hint}`);
38
+ stdout.log(`${colors.yellow("Tool hint")} ${tool.name}: ${tool.hint}`);
37
39
  if (tool.install?.length) {
38
- stdout.log(`Install ${tool.name}: ${tool.install.join(" OR ")}`);
40
+ stdout.log(`${colors.cyan(`Install ${tool.name}`)}: ${tool.install.join(" OR ")}`);
39
41
  }
40
42
  }
41
- stdout.log(`Tests: ${tests[0]?.command ?? "not detected"}`);
43
+ stdout.log(`${colors.bold("Tests")}: ${tests[0]?.command ?? colors.yellow("not detected")}`);
42
44
  for (const check of deepChecks) {
43
- stdout.log(`Deep ${check.name}: ${check.ok ? "ok" : "missing"}`);
45
+ stdout.log(`Deep ${check.name}: ${colors.status(check.ok)}`);
44
46
  }
45
47
  for (const check of contractChecks) {
46
- stdout.log(`Contract ${check.name}: ${check.ok ? "ok" : check.detail}`);
48
+ stdout.log(`Contract ${check.name}: ${check.ok ? colors.green("ok") : colors.red(check.detail)}`);
47
49
  }
48
50
  for (const adapter of adapters) {
49
- stdout.log(`Adapter ${adapter.name}: ${adapter.present}/${adapter.total} files present`);
51
+ const ok = adapter.present === adapter.total;
52
+ stdout.log(`Adapter ${adapter.name}: ${colors.status(ok, `${adapter.present}/${adapter.total}`, `${adapter.present}/${adapter.total}`)} files present`);
50
53
  }
51
54
  }
52
55
  return report.status === "ok" ? 0 : 1;
@@ -1,6 +1,7 @@
1
1
  import { spawnSync } from "node:child_process";
2
2
  import { beadsViewerInstallGuide, checkTools, commandExists } from "../adapters/tool-checks.js";
3
3
  import { prepareBeadsMirror } from "../core/beads-mirror.js";
4
+ import { createColors } from "../core/colors.js";
4
5
  import { readSyncedStories, selectableStories } from "../core/synced-stories.js";
5
6
  const robotActions = new Map([
6
7
  ["triage", "--robot-triage"],
@@ -37,6 +38,7 @@ export async function graphCommand(options) {
37
38
  }
38
39
  async function graphSetup(options) {
39
40
  const stdout = options.stdout ?? console;
41
+ const colors = createColors();
40
42
  const tools = checkTools().filter((tool) => ["br", "bd", "bv"].includes(tool.name));
41
43
  const mirrorPath = await prepareBeadsMirror(options.root);
42
44
  const report = {
@@ -51,9 +53,9 @@ async function graphSetup(options) {
51
53
  else {
52
54
  stdout.log(beadsViewerInstallGuide());
53
55
  stdout.log("");
54
- stdout.log(`Mirror: ${mirrorPath}`);
56
+ stdout.log(`${colors.bold("Mirror")}: ${colors.cyan(mirrorPath)}`);
55
57
  for (const tool of tools) {
56
- stdout.log(`${tool.name}: ${tool.available ? "ok" : "missing"} - ${tool.hint}`);
58
+ stdout.log(`${colors.bold(tool.name)}: ${colors.status(tool.available)} - ${colors.dim(tool.hint)}`);
57
59
  }
58
60
  }
59
61
  return 0;
@@ -2,8 +2,10 @@ import { coreFiles, enterpriseFiles } from "../core/scaffold.js";
2
2
  import { agentFiles } from "../core/agent-scaffold.js";
3
3
  import { writeManagedFiles } from "../core/managed-files.js";
4
4
  import { getAdapters } from "../config/adapter-registry.js";
5
+ import { createColors } from "../core/colors.js";
5
6
  export async function initCommand(options) {
6
7
  const stdout = options.stdout ?? console;
8
+ const colors = createColors();
7
9
  const selectedIde = options.ide ?? "all";
8
10
  const sharedFiles = [...coreFiles(), ...agentFiles()];
9
11
  const baseFiles = options.enterprise ? [...sharedFiles, ...enterpriseFiles()] : sharedFiles;
@@ -13,9 +15,9 @@ export async function initCommand(options) {
13
15
  const updated = results.filter((result) => result.status === "updated").length;
14
16
  const skipped = results.filter((result) => result.status === "skipped");
15
17
  const mode = options.enterprise ? "enterprise" : "standard";
16
- stdout.log(`Speckit initialized (${mode}): ${created} created, ${updated} updated, ${skipped.length} skipped.`);
18
+ stdout.log(`${colors.bold("Speckit initialized")} (${colors.cyan(mode)}): ${colors.green(String(created))} created, ${colors.yellow(String(updated))} updated, ${colors.status(skipped.length === 0, String(skipped.length), String(skipped.length))} skipped.`);
17
19
  for (const result of skipped) {
18
- stdout.log(`skipped ${result.path}: ${result.reason}`);
20
+ stdout.log(`${colors.yellow("skipped")} ${result.path}: ${result.reason}`);
19
21
  }
20
22
  return skipped.length > 0 ? 2 : 0;
21
23
  }
@@ -1,20 +1,22 @@
1
1
  import { evaluateReadiness } from "../core/readiness.js";
2
+ import { createColors } from "../core/colors.js";
2
3
  export async function readyCommand(options) {
3
4
  const stdout = options.stdout ?? console;
5
+ const colors = createColors();
4
6
  const report = await evaluateReadiness(options.root, options.target);
5
7
  if (options.json) {
6
8
  stdout.log(JSON.stringify(report, null, 2));
7
9
  return report.status === "ready" ? 0 : 1;
8
10
  }
9
- stdout.log(`# Speckit Readiness: ${report.status}`);
11
+ stdout.log(`# ${colors.bold("Speckit Readiness")}: ${colors.status(report.status === "ready", "ready", report.status)}`);
10
12
  if (report.story) {
11
- stdout.log(`Story: ${report.story.path}`);
13
+ stdout.log(`${colors.bold("Story")}: ${colors.cyan(report.story.path)}`);
12
14
  }
13
15
  for (const check of report.checks) {
14
- stdout.log(`- ${check.ok ? "ok" : "blocked"} ${check.name}: ${check.detail}`);
16
+ stdout.log(`- ${colors.status(check.ok, "ok", "blocked")} ${check.name}: ${check.detail}`);
15
17
  }
16
18
  if (report.status !== "ready") {
17
- stdout.error("Run the suggested setup commands before `speckit run`.");
19
+ stdout.error(colors.yellow("Run the suggested setup commands before `speckit run`."));
18
20
  }
19
21
  return report.status === "ready" ? 0 : 1;
20
22
  }
@@ -1,5 +1,6 @@
1
1
  import { createInterface } from "node:readline/promises";
2
2
  import { adapters } from "../config/adapter-registry.js";
3
+ import { createColors } from "../core/colors.js";
3
4
  import { graphCommand } from "./graph.js";
4
5
  import { initCommand } from "./init.js";
5
6
  const modeChoices = [
@@ -17,6 +18,7 @@ export async function setupCommand(options) {
17
18
  const stdin = options.stdin ?? process.stdin;
18
19
  const output = options.output ?? process.stdout;
19
20
  const stdout = options.stdout ?? console;
21
+ const colors = createColors(output);
20
22
  if (options.answers) {
21
23
  return setupFromAnswers(options, stdout, options.answers);
22
24
  }
@@ -26,14 +28,14 @@ export async function setupCommand(options) {
26
28
  }
27
29
  const rl = createInterface({ input: stdin, output });
28
30
  try {
29
- stdout.log("Speckit setup");
31
+ stdout.log(colors.bold(colors.cyan("Speckit setup")));
30
32
  stdout.log("");
31
- const mode = await select(rl, stdout, modeChoices, "Choose setup mode", "1");
32
- const ide = await select(rl, stdout, ideChoices, "Choose IDE adapter", "1");
33
- const force = await confirm(rl, stdout, "Overwrite unmanaged files if needed?", false);
34
- const configureGraph = await confirm(rl, stdout, "Run Beads Viewer setup after init?", true);
33
+ const mode = await select(rl, stdout, modeChoices, "Choose setup mode", "1", colors);
34
+ const ide = await select(rl, stdout, ideChoices, "Choose IDE adapter", "1", colors);
35
+ const force = await confirm(rl, stdout, "Overwrite unmanaged files if needed?", false, colors);
36
+ const configureGraph = await confirm(rl, stdout, "Run Beads Viewer setup after init?", true, colors);
35
37
  stdout.log("");
36
- stdout.log(`Selected: ${mode}, ide=${ide}, force=${force ? "yes" : "no"}, graph=${configureGraph ? "yes" : "no"}`);
38
+ stdout.log(colors.dim(`Selected: ${mode}, ide=${ide}, force=${force ? "yes" : "no"}, graph=${configureGraph ? "yes" : "no"}`));
37
39
  stdout.log("");
38
40
  const initExit = await initCommand({
39
41
  root: options.root,
@@ -53,14 +55,15 @@ export async function setupCommand(options) {
53
55
  }
54
56
  }
55
57
  async function setupFromAnswers(options, stdout, answers) {
56
- stdout.log("Speckit setup");
58
+ const colors = createColors();
59
+ stdout.log(colors.bold(colors.cyan("Speckit setup")));
57
60
  stdout.log("");
58
- const mode = selectAnswer(stdout, modeChoices, "Choose setup mode", "1", answers.shift());
59
- const ide = selectAnswer(stdout, ideChoices, "Choose IDE adapter", "1", answers.shift());
61
+ const mode = selectAnswer(stdout, modeChoices, "Choose setup mode", "1", answers.shift(), colors);
62
+ const ide = selectAnswer(stdout, ideChoices, "Choose IDE adapter", "1", answers.shift(), colors);
60
63
  const force = confirmAnswer("Overwrite unmanaged files if needed?", false, answers.shift());
61
64
  const configureGraph = confirmAnswer("Run Beads Viewer setup after init?", true, answers.shift());
62
65
  stdout.log("");
63
- stdout.log(`Selected: ${mode}, ide=${ide}, force=${force ? "yes" : "no"}, graph=${configureGraph ? "yes" : "no"}`);
66
+ stdout.log(colors.dim(`Selected: ${mode}, ide=${ide}, force=${force ? "yes" : "no"}, graph=${configureGraph ? "yes" : "no"}`));
64
67
  stdout.log("");
65
68
  const initExit = await initCommand({
66
69
  root: options.root,
@@ -75,9 +78,9 @@ async function setupFromAnswers(options, stdout, answers) {
75
78
  }
76
79
  return initExit;
77
80
  }
78
- function selectAnswer(stdout, choices, prompt, defaultValue, answer) {
79
- stdout.log(prompt);
80
- choices.forEach((choice, index) => stdout.log(` ${index + 1}. ${choice.label}`));
81
+ function selectAnswer(stdout, choices, prompt, defaultValue, answer, colors = createColors()) {
82
+ stdout.log(colors.bold(prompt));
83
+ choices.forEach((choice, index) => stdout.log(` ${colors.cyan(String(index + 1))}. ${choice.label}`));
81
84
  const selected = (answer?.trim() || defaultValue);
82
85
  const index = Number.parseInt(selected, 10) - 1;
83
86
  if (!Number.isInteger(index) || !choices[index]) {
@@ -96,33 +99,33 @@ function confirmAnswer(prompt, defaultValue, answer) {
96
99
  return false;
97
100
  throw new Error(`Invalid yes/no answer for "${prompt}": ${answer}`);
98
101
  }
99
- async function select(rl, stdout, choices, prompt, defaultValue) {
102
+ async function select(rl, stdout, choices, prompt, defaultValue, colors = createColors()) {
100
103
  for (;;) {
101
- stdout.log(prompt);
104
+ stdout.log(colors.bold(prompt));
102
105
  choices.forEach((choice, index) => {
103
106
  const number = String(index + 1);
104
- stdout.log(` ${number}. ${choice.label}`);
107
+ stdout.log(` ${colors.cyan(number)}. ${choice.label}`);
105
108
  });
106
- const answer = (await rl.question(`Select [${defaultValue}]: `)).trim() || defaultValue;
109
+ const answer = (await rl.question(`${colors.dim("Select")} [${colors.cyan(defaultValue)}]: `)).trim() || defaultValue;
107
110
  const index = Number.parseInt(answer, 10) - 1;
108
111
  if (Number.isInteger(index) && choices[index]) {
109
112
  stdout.log("");
110
113
  return choices[index].value;
111
114
  }
112
- stdout.log(`Invalid selection: ${answer}`);
115
+ stdout.log(colors.red(`Invalid selection: ${answer}`));
113
116
  stdout.log("");
114
117
  }
115
118
  }
116
- async function confirm(rl, stdout, prompt, defaultValue) {
119
+ async function confirm(rl, stdout, prompt, defaultValue, colors = createColors()) {
117
120
  const suffix = defaultValue ? "Y/n" : "y/N";
118
121
  for (;;) {
119
- const answer = (await rl.question(`${prompt} [${suffix}]: `)).trim().toLowerCase();
122
+ const answer = (await rl.question(`${prompt} [${colors.cyan(suffix)}]: `)).trim().toLowerCase();
120
123
  if (!answer)
121
124
  return defaultValue;
122
125
  if (["y", "yes"].includes(answer))
123
126
  return true;
124
127
  if (["n", "no"].includes(answer))
125
128
  return false;
126
- stdout.log("Please answer y or n.");
129
+ stdout.log(colors.yellow("Please answer y or n."));
127
130
  }
128
131
  }
@@ -3,8 +3,10 @@ import { join } from "node:path";
3
3
  import { markdown, writeManagedFiles } from "../core/managed-files.js";
4
4
  import { firstHeading, frontmatterValue } from "../core/story.js";
5
5
  import { writeBeadsMirror } from "../core/beads-mirror.js";
6
+ import { createColors } from "../core/colors.js";
6
7
  export async function syncCommand(options) {
7
8
  const stdout = options.stdout ?? console;
9
+ const colors = createColors();
8
10
  const storiesDir = join(options.root, ".speckit", "stories");
9
11
  let entries;
10
12
  try {
@@ -45,7 +47,7 @@ bv --robot-next --format json
45
47
  },
46
48
  ], true);
47
49
  const beadsPath = await writeBeadsMirror(options.root, stories);
48
- stdout.log(`Synced ${stories.length} stories to .speckit/sync/beads-sync.jsonl`);
49
- stdout.log(`Prepared Beads Viewer mirror at ${beadsPath}`);
50
+ stdout.log(`Synced ${colors.cyan(String(stories.length))} stories to ${colors.cyan(".speckit/sync/beads-sync.jsonl")}`);
51
+ stdout.log(`Prepared Beads Viewer mirror at ${colors.cyan(beadsPath)}`);
50
52
  return 0;
51
53
  }
@@ -1,6 +1,8 @@
1
1
  import { validateWorkflowContract } from "../core/workflow-validator.js";
2
+ import { createColors } from "../core/colors.js";
2
3
  export async function validateCommand(options) {
3
4
  const stdout = options.stdout ?? console;
5
+ const colors = createColors();
4
6
  const checks = await validateWorkflowContract(options.root);
5
7
  const status = checks.every((check) => check.ok) ? "ok" : "needs-attention";
6
8
  const report = { status, checks };
@@ -8,9 +10,9 @@ export async function validateCommand(options) {
8
10
  stdout.log(JSON.stringify(report, null, 2));
9
11
  }
10
12
  else {
11
- stdout.log(`Speckit workflow contract: ${status}`);
13
+ stdout.log(`${colors.bold("Speckit workflow contract")}: ${colors.status(status === "ok", "ok", status)}`);
12
14
  for (const check of checks) {
13
- stdout.log(`${check.ok ? "ok" : "fail"} ${check.name}: ${check.detail}`);
15
+ stdout.log(`${colors.status(check.ok, "ok", "fail")} ${check.name}: ${check.detail}`);
14
16
  }
15
17
  }
16
18
  return status === "ok" ? 0 : 1;
@@ -0,0 +1,11 @@
1
+ type ColorOutput = {
2
+ isTTY?: boolean;
3
+ };
4
+ type ColorName = "blue" | "cyan" | "dim" | "green" | "red" | "yellow" | "bold";
5
+ export type Colors = Record<ColorName, (text: string) => string> & {
6
+ enabled: boolean;
7
+ status(ok: boolean, okText?: string, failText?: string): string;
8
+ };
9
+ export declare function createColors(output?: ColorOutput): Colors;
10
+ export declare function colorsEnabled(output?: ColorOutput): boolean;
11
+ export {};
@@ -0,0 +1,40 @@
1
+ const ansi = {
2
+ blue: [34, 39],
3
+ cyan: [36, 39],
4
+ dim: [2, 22],
5
+ green: [32, 39],
6
+ red: [31, 39],
7
+ yellow: [33, 39],
8
+ bold: [1, 22],
9
+ };
10
+ export function createColors(output) {
11
+ const enabled = colorsEnabled(output);
12
+ const style = (name) => (text) => {
13
+ if (!enabled)
14
+ return text;
15
+ const [open, close] = ansi[name];
16
+ return `\u001B[${open}m${text}\u001B[${close}m`;
17
+ };
18
+ return {
19
+ enabled,
20
+ blue: style("blue"),
21
+ cyan: style("cyan"),
22
+ dim: style("dim"),
23
+ green: style("green"),
24
+ red: style("red"),
25
+ yellow: style("yellow"),
26
+ bold: style("bold"),
27
+ status(ok, okText = "ok", failText = "missing") {
28
+ return ok ? this.green(okText) : this.red(failText);
29
+ },
30
+ };
31
+ }
32
+ export function colorsEnabled(output) {
33
+ if (process.env.NO_COLOR || process.env.SPECKIT_COLOR === "0")
34
+ return false;
35
+ if (process.env.FORCE_COLOR === "0")
36
+ return false;
37
+ if (process.env.FORCE_COLOR || process.env.SPECKIT_COLOR === "1")
38
+ return true;
39
+ return output?.isTTY ?? process.stdout.isTTY ?? false;
40
+ }
@@ -1,5 +1,17 @@
1
1
  # Project Changelog
2
2
 
3
+ ## 0.3.6 - 2026-05-11
4
+
5
+ ### Added
6
+
7
+ - Added ANSI color rendering for interactive CLI and TUI output, including setup prompts, init summaries, doctor checks, graph setup, readiness, validation, sync, and help headers.
8
+ - Added `SPECKIT_COLOR=1` / `FORCE_COLOR` support for demos and snapshots, with `NO_COLOR` / `SPECKIT_COLOR=0` opt-out support.
9
+
10
+ ### Quality
11
+
12
+ - Added color helper regression tests.
13
+ - JSON output remains uncolored for automation.
14
+
3
15
  ## 0.3.5 - 2026-05-11
4
16
 
5
17
  ### Added
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@trieungoctam/speckit",
3
- "version": "0.3.5",
3
+ "version": "0.3.6",
4
4
  "description": "Enterprise Agile + TDD workflow compiler for agentic IDEs.",
5
5
  "type": "module",
6
6
  "files": [