@teammates/cli 0.4.1 → 0.5.1
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 +36 -4
- package/dist/adapter.d.ts +19 -3
- package/dist/adapter.js +168 -96
- package/dist/adapter.test.js +29 -16
- package/dist/adapters/cli-proxy.d.ts +3 -1
- package/dist/adapters/cli-proxy.js +65 -6
- package/dist/adapters/copilot.d.ts +3 -1
- package/dist/adapters/copilot.js +16 -3
- package/dist/adapters/echo.d.ts +3 -1
- package/dist/adapters/echo.js +4 -2
- package/dist/banner.js +5 -1
- package/dist/cli-args.js +23 -23
- package/dist/cli-args.test.d.ts +1 -0
- package/dist/cli-args.test.js +125 -0
- package/dist/cli.js +486 -220
- package/dist/compact.d.ts +23 -0
- package/dist/compact.js +181 -11
- package/dist/compact.test.js +323 -7
- package/dist/index.d.ts +4 -1
- package/dist/index.js +3 -1
- package/dist/onboard.js +165 -165
- package/dist/orchestrator.js +7 -2
- package/dist/personas.d.ts +42 -0
- package/dist/personas.js +108 -0
- package/dist/personas.test.d.ts +1 -0
- package/dist/personas.test.js +88 -0
- package/dist/registry.test.js +23 -23
- package/dist/theme.test.d.ts +1 -0
- package/dist/theme.test.js +113 -0
- package/dist/types.d.ts +2 -0
- package/package.json +4 -3
- package/personas/architect.md +95 -0
- package/personas/backend.md +97 -0
- package/personas/data-engineer.md +96 -0
- package/personas/designer.md +96 -0
- package/personas/devops.md +97 -0
- package/personas/frontend.md +98 -0
- package/personas/ml-ai.md +100 -0
- package/personas/mobile.md +97 -0
- package/personas/performance.md +96 -0
- package/personas/pm.md +93 -0
- package/personas/prompt-engineer.md +122 -0
- package/personas/qa.md +96 -0
- package/personas/security.md +96 -0
- package/personas/sre.md +97 -0
- package/personas/swe.md +92 -0
- package/personas/tech-writer.md +97 -0
|
@@ -20,7 +20,8 @@ import { mkdirSync } from "node:fs";
|
|
|
20
20
|
import { mkdir, readFile, unlink, writeFile } from "node:fs/promises";
|
|
21
21
|
import { tmpdir } from "node:os";
|
|
22
22
|
import { join } from "node:path";
|
|
23
|
-
import { buildTeammatePrompt, queryRecallContext } from "../adapter.js";
|
|
23
|
+
import { DAILY_LOG_BUDGET_TOKENS, buildTeammatePrompt, queryRecallContext, } from "../adapter.js";
|
|
24
|
+
import { autoCompactForBudget } from "../compact.js";
|
|
24
25
|
export const PRESETS = {
|
|
25
26
|
claude: {
|
|
26
27
|
name: "claude",
|
|
@@ -136,12 +137,15 @@ export class CliProxyAdapter {
|
|
|
136
137
|
this.sessionFiles.set(teammate.name, sessionFile);
|
|
137
138
|
return id;
|
|
138
139
|
}
|
|
139
|
-
async executeTask(_sessionId, teammate, prompt) {
|
|
140
|
-
// If
|
|
141
|
-
//
|
|
140
|
+
async executeTask(_sessionId, teammate, prompt, options) {
|
|
141
|
+
// If raw mode is set, skip all prompt wrapping — send prompt as-is
|
|
142
|
+
// Used for defensive retries where the full prompt template is counterproductive
|
|
142
143
|
const sessionFile = this.sessionFiles.get(teammate.name);
|
|
143
144
|
let fullPrompt;
|
|
144
|
-
if (
|
|
145
|
+
if (options?.raw) {
|
|
146
|
+
fullPrompt = prompt;
|
|
147
|
+
}
|
|
148
|
+
else if (teammate.soul) {
|
|
145
149
|
// Query recall for relevant memories before building prompt
|
|
146
150
|
const teammatesDir = teammate.cwd
|
|
147
151
|
? join(teammate.cwd, ".teammates")
|
|
@@ -149,6 +153,16 @@ export class CliProxyAdapter {
|
|
|
149
153
|
const recall = teammatesDir
|
|
150
154
|
? await queryRecallContext(teammatesDir, teammate.name, prompt)
|
|
151
155
|
: undefined;
|
|
156
|
+
// Auto-compact daily logs if they exceed the token budget
|
|
157
|
+
if (teammatesDir) {
|
|
158
|
+
const teammateDir = join(teammatesDir, teammate.name);
|
|
159
|
+
const compacted = await autoCompactForBudget(teammateDir, DAILY_LOG_BUDGET_TOKENS);
|
|
160
|
+
if (compacted) {
|
|
161
|
+
// Filter compacted dates out of in-memory daily logs
|
|
162
|
+
const compactedSet = new Set(compacted.compactedDates);
|
|
163
|
+
teammate.dailyLogs = teammate.dailyLogs.filter((log) => !compactedSet.has(log.date));
|
|
164
|
+
}
|
|
165
|
+
}
|
|
152
166
|
// Read USER.md for injection into the prompt
|
|
153
167
|
let userProfile;
|
|
154
168
|
if (teammatesDir) {
|
|
@@ -478,8 +492,12 @@ function parseMessageProtocol(output, teammateName, _teammateNames) {
|
|
|
478
492
|
break;
|
|
479
493
|
}
|
|
480
494
|
}
|
|
481
|
-
// Find all ```handoff blocks
|
|
495
|
+
// Find all ```handoff blocks (primary) + natural-language fallback
|
|
482
496
|
const handoffBlocks = findHandoffBlocks(output);
|
|
497
|
+
if (handoffBlocks.length === 0) {
|
|
498
|
+
// Fallback: detect natural-language handoff patterns mentioning known teammates
|
|
499
|
+
handoffBlocks.push(...findNaturalLanguageHandoffs(output, _teammateNames));
|
|
500
|
+
}
|
|
483
501
|
const handoffs = handoffBlocks.map((h) => ({
|
|
484
502
|
from: teammateName,
|
|
485
503
|
to: h.target,
|
|
@@ -520,6 +538,47 @@ function findHandoffBlocks(output) {
|
|
|
520
538
|
}
|
|
521
539
|
return results;
|
|
522
540
|
}
|
|
541
|
+
/**
|
|
542
|
+
* Fallback handoff detector: catches natural-language handoff patterns when
|
|
543
|
+
* the agent fails to use the ```handoff fenced block format.
|
|
544
|
+
*
|
|
545
|
+
* Looks for sentences like:
|
|
546
|
+
* - "hand off to @beacon: implement the feature"
|
|
547
|
+
* - "handing this to @scribe for documentation"
|
|
548
|
+
* - "I'll delegate to @pipeline"
|
|
549
|
+
* - "queued a handoff to @beacon"
|
|
550
|
+
*
|
|
551
|
+
* Only triggers if the @mentioned name is in the known teammate list.
|
|
552
|
+
* Extracts the surrounding sentence as the task description.
|
|
553
|
+
*/
|
|
554
|
+
function findNaturalLanguageHandoffs(output, teammateNames) {
|
|
555
|
+
if (teammateNames.length === 0)
|
|
556
|
+
return [];
|
|
557
|
+
const results = [];
|
|
558
|
+
const seen = new Set();
|
|
559
|
+
// Pattern: handoff-related verb/noun near @teammate
|
|
560
|
+
const pattern = /(?:hand(?:off|ing off| off| this off)|delegat(?:e|ing)|pass(?:ing)? (?:this |it )?(?:to|off to)|queued? (?:a )?handoff (?:to|for))\s+@(\w+)\b[.:,]?\s*(.*)/gi;
|
|
561
|
+
let match;
|
|
562
|
+
while ((match = pattern.exec(output)) !== null) {
|
|
563
|
+
const target = match[1].toLowerCase();
|
|
564
|
+
if (!teammateNames.includes(target))
|
|
565
|
+
continue;
|
|
566
|
+
if (seen.has(target))
|
|
567
|
+
continue;
|
|
568
|
+
seen.add(target);
|
|
569
|
+
// Use the rest of the sentence as the task, or a generic description
|
|
570
|
+
let task = match[2]
|
|
571
|
+
.replace(/\n.*/s, "") // first line only
|
|
572
|
+
.replace(/[.!]+$/, "") // strip trailing punctuation
|
|
573
|
+
.trim();
|
|
574
|
+
if (!task || task.length < 5) {
|
|
575
|
+
task =
|
|
576
|
+
"(handoff detected from natural language — no task details provided)";
|
|
577
|
+
}
|
|
578
|
+
results.push({ target, task });
|
|
579
|
+
}
|
|
580
|
+
return results;
|
|
581
|
+
}
|
|
523
582
|
/** Extract file paths from agent output. */
|
|
524
583
|
export function parseChangedFiles(output) {
|
|
525
584
|
const files = new Set();
|
|
@@ -40,7 +40,9 @@ export declare class CopilotAdapter implements AgentAdapter {
|
|
|
40
40
|
private sessionsDir;
|
|
41
41
|
constructor(options?: CopilotAdapterOptions);
|
|
42
42
|
startSession(teammate: TeammateConfig): Promise<string>;
|
|
43
|
-
executeTask(_sessionId: string, teammate: TeammateConfig, prompt: string
|
|
43
|
+
executeTask(_sessionId: string, teammate: TeammateConfig, prompt: string, options?: {
|
|
44
|
+
raw?: boolean;
|
|
45
|
+
}): Promise<TaskResult>;
|
|
44
46
|
routeTask(task: string, roster: RosterEntry[]): Promise<string | null>;
|
|
45
47
|
getSessionFile(teammateName: string): string | undefined;
|
|
46
48
|
destroySession(_sessionId: string): Promise<void>;
|
package/dist/adapters/copilot.js
CHANGED
|
@@ -12,7 +12,8 @@
|
|
|
12
12
|
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
13
13
|
import { join } from "node:path";
|
|
14
14
|
import { approveAll, CopilotClient, } from "@github/copilot-sdk";
|
|
15
|
-
import { buildTeammatePrompt, queryRecallContext } from "../adapter.js";
|
|
15
|
+
import { DAILY_LOG_BUDGET_TOKENS, buildTeammatePrompt, queryRecallContext, } from "../adapter.js";
|
|
16
|
+
import { autoCompactForBudget } from "../compact.js";
|
|
16
17
|
import { parseResult } from "./cli-proxy.js";
|
|
17
18
|
// ─── Adapter ─────────────────────────────────────────────────────────
|
|
18
19
|
let nextId = 1;
|
|
@@ -56,12 +57,15 @@ export class CopilotAdapter {
|
|
|
56
57
|
this.sessionFiles.set(teammate.name, sessionFile);
|
|
57
58
|
return id;
|
|
58
59
|
}
|
|
59
|
-
async executeTask(_sessionId, teammate, prompt) {
|
|
60
|
+
async executeTask(_sessionId, teammate, prompt, options) {
|
|
60
61
|
await this.ensureClient(teammate.cwd);
|
|
61
62
|
const sessionFile = this.sessionFiles.get(teammate.name);
|
|
62
63
|
// Build the full teammate prompt (identity + memory + task)
|
|
63
64
|
let fullPrompt;
|
|
64
|
-
if (
|
|
65
|
+
if (options?.raw) {
|
|
66
|
+
fullPrompt = prompt;
|
|
67
|
+
}
|
|
68
|
+
else if (teammate.soul) {
|
|
65
69
|
// Query recall for relevant memories before building prompt
|
|
66
70
|
const teammatesDir = teammate.cwd
|
|
67
71
|
? join(teammate.cwd, ".teammates")
|
|
@@ -69,6 +73,15 @@ export class CopilotAdapter {
|
|
|
69
73
|
const recall = teammatesDir
|
|
70
74
|
? await queryRecallContext(teammatesDir, teammate.name, prompt)
|
|
71
75
|
: undefined;
|
|
76
|
+
// Auto-compact daily logs if they exceed the token budget
|
|
77
|
+
if (teammatesDir) {
|
|
78
|
+
const teammateDir = join(teammatesDir, teammate.name);
|
|
79
|
+
const compacted = await autoCompactForBudget(teammateDir, DAILY_LOG_BUDGET_TOKENS);
|
|
80
|
+
if (compacted) {
|
|
81
|
+
const compactedSet = new Set(compacted.compactedDates);
|
|
82
|
+
teammate.dailyLogs = teammate.dailyLogs.filter((log) => !compactedSet.has(log.date));
|
|
83
|
+
}
|
|
84
|
+
}
|
|
72
85
|
// Read USER.md for injection into the prompt
|
|
73
86
|
let userProfile;
|
|
74
87
|
if (teammatesDir) {
|
package/dist/adapters/echo.d.ts
CHANGED
|
@@ -9,5 +9,7 @@ import type { TaskResult, TeammateConfig } from "../types.js";
|
|
|
9
9
|
export declare class EchoAdapter implements AgentAdapter {
|
|
10
10
|
readonly name = "echo";
|
|
11
11
|
startSession(teammate: TeammateConfig): Promise<string>;
|
|
12
|
-
executeTask(_sessionId: string, teammate: TeammateConfig, prompt: string
|
|
12
|
+
executeTask(_sessionId: string, teammate: TeammateConfig, prompt: string, options?: {
|
|
13
|
+
raw?: boolean;
|
|
14
|
+
}): Promise<TaskResult>;
|
|
13
15
|
}
|
package/dist/adapters/echo.js
CHANGED
|
@@ -11,8 +11,10 @@ export class EchoAdapter {
|
|
|
11
11
|
async startSession(teammate) {
|
|
12
12
|
return `echo-${teammate.name}-${nextId++}`;
|
|
13
13
|
}
|
|
14
|
-
async executeTask(_sessionId, teammate, prompt) {
|
|
15
|
-
const fullPrompt =
|
|
14
|
+
async executeTask(_sessionId, teammate, prompt, options) {
|
|
15
|
+
const fullPrompt = options?.raw
|
|
16
|
+
? prompt
|
|
17
|
+
: buildTeammatePrompt(teammate, prompt);
|
|
16
18
|
return {
|
|
17
19
|
teammate: teammate.name,
|
|
18
20
|
success: true,
|
package/dist/banner.js
CHANGED
|
@@ -78,7 +78,11 @@ export class AnimatedBanner extends Control {
|
|
|
78
78
|
// Service status rows
|
|
79
79
|
for (const svc of info.services) {
|
|
80
80
|
const isBundledOrConfigured = svc.status === "bundled" || svc.status === "configured";
|
|
81
|
-
const icon = isBundledOrConfigured
|
|
81
|
+
const icon = isBundledOrConfigured
|
|
82
|
+
? "● "
|
|
83
|
+
: svc.status === "not-configured"
|
|
84
|
+
? "◐ "
|
|
85
|
+
: "○ ";
|
|
82
86
|
const color = isBundledOrConfigured ? tp.success : tp.warning;
|
|
83
87
|
const label = svc.status === "bundled"
|
|
84
88
|
? "bundled"
|
package/dist/cli-args.js
CHANGED
|
@@ -97,28 +97,28 @@ export async function resolveAdapter(name, opts = {}) {
|
|
|
97
97
|
}
|
|
98
98
|
// ─── Usage ───────────────────────────────────────────────────────────
|
|
99
99
|
export function printUsage() {
|
|
100
|
-
console.log(`
|
|
101
|
-
${chalk.bold("@teammates/cli")} — Agent-agnostic teammate orchestrator
|
|
102
|
-
|
|
103
|
-
${chalk.bold("Usage:")}
|
|
104
|
-
teammates <agent> Launch session with an agent
|
|
105
|
-
teammates codex Use OpenAI Codex
|
|
106
|
-
teammates aider Use Aider
|
|
107
|
-
|
|
108
|
-
${chalk.bold("Options:")}
|
|
109
|
-
--model <model> Override the agent model
|
|
110
|
-
--dir <path> Override .teammates/ location
|
|
111
|
-
|
|
112
|
-
${chalk.bold("Agents:")}
|
|
113
|
-
claude Claude Code CLI (requires 'claude' on PATH)
|
|
114
|
-
codex OpenAI Codex CLI (requires 'codex' on PATH)
|
|
115
|
-
aider Aider CLI (requires 'aider' on PATH)
|
|
116
|
-
echo Test adapter — echoes prompts (no external agent)
|
|
117
|
-
|
|
118
|
-
${chalk.bold("In-session:")}
|
|
119
|
-
@teammate <task> Assign directly via @mention
|
|
120
|
-
<text> Auto-route to the best teammate
|
|
121
|
-
/status Session overview
|
|
122
|
-
/help All commands
|
|
100
|
+
console.log(`
|
|
101
|
+
${chalk.bold("@teammates/cli")} — Agent-agnostic teammate orchestrator
|
|
102
|
+
|
|
103
|
+
${chalk.bold("Usage:")}
|
|
104
|
+
teammates <agent> Launch session with an agent
|
|
105
|
+
teammates codex Use OpenAI Codex
|
|
106
|
+
teammates aider Use Aider
|
|
107
|
+
|
|
108
|
+
${chalk.bold("Options:")}
|
|
109
|
+
--model <model> Override the agent model
|
|
110
|
+
--dir <path> Override .teammates/ location
|
|
111
|
+
|
|
112
|
+
${chalk.bold("Agents:")}
|
|
113
|
+
claude Claude Code CLI (requires 'claude' on PATH)
|
|
114
|
+
codex OpenAI Codex CLI (requires 'codex' on PATH)
|
|
115
|
+
aider Aider CLI (requires 'aider' on PATH)
|
|
116
|
+
echo Test adapter — echoes prompts (no external agent)
|
|
117
|
+
|
|
118
|
+
${chalk.bold("In-session:")}
|
|
119
|
+
@teammate <task> Assign directly via @mention
|
|
120
|
+
<text> Auto-route to the best teammate
|
|
121
|
+
/status Session overview
|
|
122
|
+
/help All commands
|
|
123
123
|
`.trim());
|
|
124
124
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { mkdir, rm } from "node:fs/promises";
|
|
2
|
+
import { tmpdir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
5
|
+
import { findTeammatesDir, PKG_VERSION, parseCliArgs, printUsage, } from "./cli-args.js";
|
|
6
|
+
describe("parseCliArgs", () => {
|
|
7
|
+
it("returns defaults with no arguments", () => {
|
|
8
|
+
const result = parseCliArgs([]);
|
|
9
|
+
expect(result).toEqual({
|
|
10
|
+
showHelp: false,
|
|
11
|
+
modelOverride: undefined,
|
|
12
|
+
dirOverride: undefined,
|
|
13
|
+
adapterName: "echo",
|
|
14
|
+
agentPassthrough: [],
|
|
15
|
+
});
|
|
16
|
+
});
|
|
17
|
+
it("parses --help flag", () => {
|
|
18
|
+
const result = parseCliArgs(["--help"]);
|
|
19
|
+
expect(result.showHelp).toBe(true);
|
|
20
|
+
});
|
|
21
|
+
it("parses --model option with value", () => {
|
|
22
|
+
const result = parseCliArgs(["--model", "gpt-4"]);
|
|
23
|
+
expect(result.modelOverride).toBe("gpt-4");
|
|
24
|
+
});
|
|
25
|
+
it("parses --dir option with value", () => {
|
|
26
|
+
const result = parseCliArgs(["--dir", "/custom/path"]);
|
|
27
|
+
expect(result.dirOverride).toBe("/custom/path");
|
|
28
|
+
});
|
|
29
|
+
it("extracts adapter name as first positional argument", () => {
|
|
30
|
+
const result = parseCliArgs(["claude"]);
|
|
31
|
+
expect(result.adapterName).toBe("claude");
|
|
32
|
+
});
|
|
33
|
+
it("passes remaining args as agentPassthrough", () => {
|
|
34
|
+
const result = parseCliArgs(["claude", "--verbose", "--debug"]);
|
|
35
|
+
expect(result.adapterName).toBe("claude");
|
|
36
|
+
expect(result.agentPassthrough).toEqual(["--verbose", "--debug"]);
|
|
37
|
+
});
|
|
38
|
+
it("handles all options together", () => {
|
|
39
|
+
const result = parseCliArgs([
|
|
40
|
+
"--help",
|
|
41
|
+
"--model",
|
|
42
|
+
"sonnet",
|
|
43
|
+
"--dir",
|
|
44
|
+
"/tmp/tm",
|
|
45
|
+
"codex",
|
|
46
|
+
"--extra",
|
|
47
|
+
]);
|
|
48
|
+
expect(result.showHelp).toBe(true);
|
|
49
|
+
expect(result.modelOverride).toBe("sonnet");
|
|
50
|
+
expect(result.dirOverride).toBe("/tmp/tm");
|
|
51
|
+
expect(result.adapterName).toBe("codex");
|
|
52
|
+
expect(result.agentPassthrough).toEqual(["--extra"]);
|
|
53
|
+
});
|
|
54
|
+
it("does not consume --model when no value follows", () => {
|
|
55
|
+
const result = parseCliArgs(["--model"]);
|
|
56
|
+
// getOption finds --model but no value after it, so it's left in args
|
|
57
|
+
// args.shift() then picks it up as the adapter name
|
|
58
|
+
expect(result.modelOverride).toBeUndefined();
|
|
59
|
+
expect(result.adapterName).toBe("--model");
|
|
60
|
+
});
|
|
61
|
+
it("does not treat --help value as an option value", () => {
|
|
62
|
+
const result = parseCliArgs(["--help", "claude"]);
|
|
63
|
+
expect(result.showHelp).toBe(true);
|
|
64
|
+
expect(result.adapterName).toBe("claude");
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
// ── PKG_VERSION ────────────────────────────────────────────────────
|
|
68
|
+
describe("PKG_VERSION", () => {
|
|
69
|
+
it("is a valid semver-like string", () => {
|
|
70
|
+
expect(PKG_VERSION).toMatch(/^\d+\.\d+\.\d+/);
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
// ── findTeammatesDir ───────────────────────────────────────────────
|
|
74
|
+
describe("findTeammatesDir", () => {
|
|
75
|
+
let testDir;
|
|
76
|
+
beforeEach(async () => {
|
|
77
|
+
testDir = join(tmpdir(), `cli-args-test-${Date.now()}-${Math.random().toString(36).slice(2)}`);
|
|
78
|
+
await mkdir(testDir, { recursive: true });
|
|
79
|
+
});
|
|
80
|
+
afterEach(async () => {
|
|
81
|
+
await rm(testDir, { recursive: true, force: true });
|
|
82
|
+
});
|
|
83
|
+
it("returns dirOverride when provided", async () => {
|
|
84
|
+
const result = await findTeammatesDir("/some/custom/dir");
|
|
85
|
+
expect(result).toContain("some");
|
|
86
|
+
});
|
|
87
|
+
it("returns .teammates dir when it exists under cwd", async () => {
|
|
88
|
+
const tmDir = join(testDir, ".teammates");
|
|
89
|
+
await mkdir(tmDir, { recursive: true });
|
|
90
|
+
// Mock process.cwd to return our test dir
|
|
91
|
+
const origCwd = process.cwd;
|
|
92
|
+
process.cwd = () => testDir;
|
|
93
|
+
try {
|
|
94
|
+
const result = await findTeammatesDir(undefined);
|
|
95
|
+
expect(result).toBe(tmDir);
|
|
96
|
+
}
|
|
97
|
+
finally {
|
|
98
|
+
process.cwd = origCwd;
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
it("returns null when no .teammates dir exists", async () => {
|
|
102
|
+
const origCwd = process.cwd;
|
|
103
|
+
process.cwd = () => testDir;
|
|
104
|
+
try {
|
|
105
|
+
const result = await findTeammatesDir(undefined);
|
|
106
|
+
expect(result).toBeNull();
|
|
107
|
+
}
|
|
108
|
+
finally {
|
|
109
|
+
process.cwd = origCwd;
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
});
|
|
113
|
+
// ── printUsage ────────────────────────────────────────────────────
|
|
114
|
+
describe("printUsage", () => {
|
|
115
|
+
it("prints usage text to console", () => {
|
|
116
|
+
const spy = vi.spyOn(console, "log").mockImplementation(() => { });
|
|
117
|
+
printUsage();
|
|
118
|
+
expect(spy).toHaveBeenCalled();
|
|
119
|
+
const output = spy.mock.calls[0][0];
|
|
120
|
+
expect(output).toContain("teammates");
|
|
121
|
+
expect(output).toContain("--model");
|
|
122
|
+
expect(output).toContain("--dir");
|
|
123
|
+
spy.mockRestore();
|
|
124
|
+
});
|
|
125
|
+
});
|