team-toon-tack 1.0.12 → 1.6.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.
Files changed (48) hide show
  1. package/README.md +59 -8
  2. package/README.zh-TW.md +111 -22
  3. package/{bin → dist/bin}/cli.js +41 -19
  4. package/dist/scripts/config/filters.d.ts +2 -0
  5. package/dist/scripts/config/filters.js +71 -0
  6. package/dist/scripts/config/show.d.ts +2 -0
  7. package/dist/scripts/config/show.js +33 -0
  8. package/dist/scripts/config/status.d.ts +2 -0
  9. package/dist/scripts/config/status.js +73 -0
  10. package/dist/scripts/config/teams.d.ts +2 -0
  11. package/dist/scripts/config/teams.js +59 -0
  12. package/dist/scripts/config.js +47 -0
  13. package/dist/scripts/done-job.js +169 -0
  14. package/dist/scripts/init.d.ts +2 -0
  15. package/dist/scripts/init.js +369 -0
  16. package/dist/scripts/lib/config-builder.d.ts +41 -0
  17. package/dist/scripts/lib/config-builder.js +116 -0
  18. package/dist/scripts/lib/display.d.ts +12 -0
  19. package/dist/scripts/lib/display.js +91 -0
  20. package/dist/scripts/lib/git.d.ts +10 -0
  21. package/dist/scripts/lib/git.js +78 -0
  22. package/dist/scripts/lib/linear.d.ts +11 -0
  23. package/dist/scripts/lib/linear.js +61 -0
  24. package/dist/scripts/status.d.ts +2 -0
  25. package/dist/scripts/status.js +162 -0
  26. package/dist/scripts/sync.js +247 -0
  27. package/{scripts → dist/scripts}/utils.d.ts +11 -3
  28. package/{scripts → dist/scripts}/utils.js +33 -27
  29. package/dist/scripts/work-on.js +102 -0
  30. package/package.json +52 -50
  31. package/templates/claude-code-commands/done-job.md +45 -0
  32. package/templates/claude-code-commands/sync-linear.md +32 -0
  33. package/templates/claude-code-commands/work-on.md +41 -0
  34. package/bin/cli.ts +0 -125
  35. package/scripts/done-job.js +0 -230
  36. package/scripts/done-job.ts +0 -263
  37. package/scripts/init.js +0 -331
  38. package/scripts/init.ts +0 -375
  39. package/scripts/sync.js +0 -178
  40. package/scripts/sync.ts +0 -211
  41. package/scripts/utils.ts +0 -236
  42. package/scripts/work-on.js +0 -138
  43. package/scripts/work-on.ts +0 -161
  44. /package/{bin → dist/bin}/cli.d.ts +0 -0
  45. /package/{scripts/init.d.ts → dist/scripts/config.d.ts} +0 -0
  46. /package/{scripts → dist/scripts}/done-job.d.ts +0 -0
  47. /package/{scripts → dist/scripts}/sync.d.ts +0 -0
  48. /package/{scripts → dist/scripts}/work-on.d.ts +0 -0
package/scripts/sync.ts DELETED
@@ -1,211 +0,0 @@
1
- import { getLinearClient, loadConfig, loadLocalConfig, loadCycleData, saveCycleData, saveConfig, getTeamId, getPrioritySortIndex, CycleData, Task, Attachment, Comment, CycleInfo } from './utils.js';
2
-
3
- async function sync() {
4
- const args = process.argv.slice(2);
5
-
6
- // Handle help flag
7
- if (args.includes('--help') || args.includes('-h')) {
8
- console.log(`Usage: ttt sync
9
-
10
- Sync issues from Linear to local cycle.ttt file.
11
-
12
- What it does:
13
- - Fetches active cycle from Linear
14
- - Downloads all issues matching configured label
15
- - Preserves local status for existing tasks
16
- - Updates config with new cycle info
17
-
18
- Examples:
19
- ttt sync # Sync in current directory
20
- ttt sync -d .ttt # Sync using .ttt directory`);
21
- process.exit(0);
22
- }
23
-
24
- const config = await loadConfig();
25
- const localConfig = await loadLocalConfig();
26
- const client = getLinearClient();
27
- const teamId = getTeamId(config, localConfig.team);
28
-
29
- // Build excluded emails from local config
30
- const excludedEmails = new Set(
31
- (localConfig.exclude_assignees ?? [])
32
- .map(key => config.users[key]?.email)
33
- .filter(Boolean)
34
- );
35
-
36
- // Phase 1: Fetch active cycle directly from team
37
- console.log('Fetching latest cycle...');
38
- const team = await client.team(teamId);
39
- const activeCycle = await team.activeCycle;
40
-
41
- if (!activeCycle) {
42
- console.error('No active cycle found.');
43
- process.exit(1);
44
- }
45
-
46
- const cycleId = activeCycle.id;
47
- const cycleName = activeCycle.name ?? `Cycle #${activeCycle.number}`;
48
- const newCycleInfo: CycleInfo = {
49
- id: cycleId,
50
- name: cycleName,
51
- start_date: activeCycle.startsAt?.toISOString().split('T')[0] ?? '',
52
- end_date: activeCycle.endsAt?.toISOString().split('T')[0] ?? ''
53
- };
54
-
55
- // Check if cycle changed and update config with history
56
- const existingData = await loadCycleData();
57
- const oldCycleId = config.current_cycle?.id ?? existingData?.cycleId;
58
-
59
- if (oldCycleId && oldCycleId !== cycleId) {
60
- const oldCycleName = config.current_cycle?.name ?? existingData?.cycleName ?? 'Unknown';
61
- console.log(`Cycle changed: ${oldCycleName} → ${cycleName}`);
62
-
63
- // Move old cycle to history (avoid duplicates)
64
- if (config.current_cycle) {
65
- config.cycle_history = config.cycle_history ?? [];
66
- // Remove if already exists in history
67
- config.cycle_history = config.cycle_history.filter(c => c.id !== config.current_cycle!.id);
68
- config.cycle_history.unshift(config.current_cycle);
69
- // Keep only last 10 cycles
70
- if (config.cycle_history.length > 10) {
71
- config.cycle_history = config.cycle_history.slice(0, 10);
72
- }
73
- }
74
-
75
- // Update current cycle
76
- config.current_cycle = newCycleInfo;
77
- await saveConfig(config);
78
- console.log('Config updated with new cycle (old cycle saved to history).');
79
- } else {
80
- // Update current cycle info even if ID unchanged (dates might change)
81
- if (!config.current_cycle || config.current_cycle.id !== cycleId) {
82
- config.current_cycle = newCycleInfo;
83
- await saveConfig(config);
84
- }
85
- console.log(`Current cycle: ${cycleName}`);
86
- }
87
-
88
- // Phase 2: Fetch workflow states
89
- const workflowStates = await client.workflowStates({
90
- filter: { team: { id: { eq: teamId } } }
91
- });
92
- const stateMap = new Map(workflowStates.nodes.map(s => [s.name, s.id]));
93
- const testingStateId = stateMap.get('Testing');
94
-
95
- // Phase 3: Build existing tasks map for preserving local status
96
- const existingTasksMap = new Map(existingData?.tasks.map(t => [t.id, t]));
97
-
98
- // Phase 4: Fetch current issues with full content
99
- const filterLabel = localConfig.label ?? 'Frontend';
100
- console.log(`Fetching issues with label: ${filterLabel}...`);
101
- const issues = await client.issues({
102
- filter: {
103
- team: { id: { eq: teamId } },
104
- cycle: { id: { eq: cycleId } },
105
- labels: { name: { eq: filterLabel } },
106
- state: { name: { in: ["Todo", "In Progress"] } }
107
- },
108
- first: 50
109
- });
110
-
111
- if (issues.nodes.length === 0) {
112
- console.log(`No ${filterLabel} issues found in current cycle with Todo/In Progress status.`);
113
- }
114
-
115
- const tasks: Task[] = [];
116
- let updatedCount = 0;
117
-
118
- for (const issue of issues.nodes) {
119
- const assignee = await issue.assignee;
120
- const assigneeEmail = assignee?.email;
121
-
122
- // Skip excluded assignees
123
- if (assigneeEmail && excludedEmails.has(assigneeEmail)) {
124
- continue;
125
- }
126
-
127
- const labels = await issue.labels();
128
- const state = await issue.state;
129
- const parent = await issue.parent;
130
- const attachmentsData = await issue.attachments();
131
- const commentsData = await issue.comments();
132
-
133
- // Build attachments list
134
- const attachments: Attachment[] = attachmentsData.nodes.map(a => ({
135
- id: a.id,
136
- title: a.title,
137
- url: a.url,
138
- sourceType: a.sourceType ?? undefined
139
- }));
140
-
141
- // Build comments list
142
- const comments: Comment[] = await Promise.all(
143
- commentsData.nodes.map(async c => {
144
- const user = await c.user;
145
- return {
146
- id: c.id,
147
- body: c.body,
148
- createdAt: c.createdAt.toISOString(),
149
- user: user?.displayName ?? user?.email
150
- };
151
- })
152
- );
153
-
154
- let localStatus: Task['localStatus'] = 'pending';
155
-
156
- // Preserve local status & sync completed tasks to Linear
157
- if (existingTasksMap.has(issue.identifier)) {
158
- const existing = existingTasksMap.get(issue.identifier)!;
159
- localStatus = existing.localStatus;
160
-
161
- if (localStatus === 'completed' && state && testingStateId) {
162
- if (!['Testing', 'Done', 'In Review', 'Canceled'].includes(state.name)) {
163
- console.log(`Updating ${issue.identifier} to Testing in Linear...`);
164
- await client.updateIssue(issue.id, { stateId: testingStateId });
165
- updatedCount++;
166
- }
167
- }
168
- }
169
-
170
- const task: Task = {
171
- id: issue.identifier,
172
- linearId: issue.id,
173
- title: issue.title,
174
- status: state ? state.name : 'Unknown',
175
- localStatus: localStatus,
176
- assignee: assigneeEmail,
177
- priority: issue.priority,
178
- labels: labels.nodes.map(l => l.name),
179
- branch: issue.branchName,
180
- description: issue.description,
181
- parentIssueId: parent ? parent.identifier : undefined,
182
- url: issue.url,
183
- attachments: attachments.length > 0 ? attachments : undefined,
184
- comments: comments.length > 0 ? comments : undefined
185
- };
186
-
187
- tasks.push(task);
188
- }
189
-
190
- // Sort by priority using config order
191
- tasks.sort((a, b) => {
192
- const pa = getPrioritySortIndex(a.priority, config.priority_order);
193
- const pb = getPrioritySortIndex(b.priority, config.priority_order);
194
- return pa - pb;
195
- });
196
-
197
- const newData: CycleData = {
198
- cycleId: cycleId,
199
- cycleName: cycleName,
200
- updatedAt: new Date().toISOString(),
201
- tasks: tasks
202
- };
203
-
204
- await saveCycleData(newData);
205
- console.log(`\n✅ Synced ${tasks.length} tasks for ${cycleName}.`);
206
- if (updatedCount > 0) {
207
- console.log(` Updated ${updatedCount} issues to Testing in Linear.`);
208
- }
209
- }
210
-
211
- sync().catch(console.error);
package/scripts/utils.ts DELETED
@@ -1,236 +0,0 @@
1
- import fs from 'node:fs/promises';
2
- import path from 'node:path';
3
- import { LinearClient } from '@linear/sdk';
4
- import { decode, encode } from '@toon-format/toon';
5
-
6
- // Resolve base directory - supports multiple configuration methods
7
- function getBaseDir(): string {
8
- // 1. Check for TOON_DIR environment variable (set by CLI or user)
9
- if (process.env.TOON_DIR) {
10
- return path.resolve(process.env.TOON_DIR);
11
- }
12
-
13
- // 2. Check for legacy LINEAR_TOON_DIR environment variable
14
- if (process.env.LINEAR_TOON_DIR) {
15
- return path.resolve(process.env.LINEAR_TOON_DIR);
16
- }
17
-
18
- // 3. Default: .ttt directory in current working directory
19
- return path.join(process.cwd(), '.ttt');
20
- }
21
-
22
- const BASE_DIR = getBaseDir();
23
- const CONFIG_PATH = path.join(BASE_DIR, 'config.toon');
24
- const CYCLE_PATH = path.join(BASE_DIR, 'cycle.toon');
25
- const LOCAL_PATH = path.join(BASE_DIR, 'local.toon');
26
-
27
- export function getPaths() {
28
- return {
29
- baseDir: BASE_DIR,
30
- configPath: CONFIG_PATH,
31
- cyclePath: CYCLE_PATH,
32
- localPath: LOCAL_PATH,
33
- };
34
- }
35
-
36
- export interface TeamConfig {
37
- id: string;
38
- name: string;
39
- icon?: string;
40
- }
41
-
42
- export interface UserConfig {
43
- id: string;
44
- email: string;
45
- displayName: string;
46
- role?: string;
47
- }
48
-
49
- export interface LabelConfig {
50
- id: string;
51
- name: string;
52
- color?: string;
53
- }
54
-
55
- export interface CycleInfo {
56
- id: string;
57
- name: string;
58
- start_date: string;
59
- end_date: string;
60
- }
61
-
62
- export interface Config {
63
- teams: Record<string, TeamConfig>;
64
- users: Record<string, UserConfig>;
65
- labels?: Record<string, LabelConfig>;
66
- priorities?: Record<string, { value: number; name: string }>;
67
- statuses?: Record<string, { name: string; type: string }>;
68
- status_transitions?: Record<string, string>;
69
- priority_order?: string[]; // e.g., ['urgent', 'high', 'medium', 'low', 'none']
70
- current_cycle?: CycleInfo;
71
- cycle_history?: CycleInfo[];
72
- }
73
-
74
- // Linear priority value to name mapping (fixed by Linear API)
75
- export const PRIORITY_NAMES: Record<number, string> = {
76
- 0: 'none',
77
- 1: 'urgent',
78
- 2: 'high',
79
- 3: 'medium',
80
- 4: 'low'
81
- };
82
-
83
- export const DEFAULT_PRIORITY_ORDER = ['urgent', 'high', 'medium', 'low', 'none'];
84
-
85
- export function getPrioritySortIndex(priority: number, priorityOrder?: string[]): number {
86
- const order = priorityOrder ?? DEFAULT_PRIORITY_ORDER;
87
- const name = PRIORITY_NAMES[priority] ?? 'none';
88
- const index = order.indexOf(name);
89
- return index === -1 ? order.length : index;
90
- }
91
-
92
- export interface Attachment {
93
- id: string;
94
- title: string;
95
- url: string;
96
- sourceType?: string;
97
- }
98
-
99
- export interface Comment {
100
- id: string;
101
- body: string;
102
- createdAt: string;
103
- user?: string;
104
- }
105
-
106
- export interface Task {
107
- id: string;
108
- linearId: string;
109
- title: string;
110
- status: string;
111
- localStatus: 'pending' | 'in-progress' | 'completed' | 'blocked-backend';
112
- assignee?: string;
113
- priority: number;
114
- labels: string[];
115
- branch?: string;
116
- description?: string;
117
- parentIssueId?: string;
118
- subIssues?: Task[];
119
- url?: string;
120
- attachments?: Attachment[];
121
- comments?: Comment[];
122
- }
123
-
124
- export interface CycleData {
125
- cycleId: string;
126
- cycleName: string;
127
- updatedAt: string;
128
- tasks: Task[];
129
- }
130
-
131
- export interface LocalConfig {
132
- current_user: string;
133
- team: string; // team key from config.teams
134
- exclude_assignees?: string[];
135
- label?: string;
136
- }
137
-
138
- export async function fileExists(filePath: string): Promise<boolean> {
139
- try {
140
- await fs.access(filePath);
141
- return true;
142
- } catch {
143
- return false;
144
- }
145
- }
146
-
147
- export async function loadConfig(): Promise<Config> {
148
- try {
149
- const fileContent = await fs.readFile(CONFIG_PATH, 'utf-8');
150
- return decode(fileContent) as unknown as Config;
151
- } catch (error) {
152
- console.error(`Error loading config from ${CONFIG_PATH}:`, error);
153
- console.error('Run `bun run init` to create configuration files.');
154
- process.exit(1);
155
- }
156
- }
157
-
158
- export async function loadLocalConfig(): Promise<LocalConfig> {
159
- try {
160
- const fileContent = await fs.readFile(LOCAL_PATH, 'utf-8');
161
- return decode(fileContent) as unknown as LocalConfig;
162
- } catch {
163
- console.error(`Error: ${LOCAL_PATH} not found.`);
164
- console.error('Run `bun run init` to create local configuration.');
165
- process.exit(1);
166
- }
167
- }
168
-
169
- export async function getUserEmail(): Promise<string> {
170
- const localConfig = await loadLocalConfig();
171
- const config = await loadConfig();
172
- const user = config.users[localConfig.current_user];
173
- if (!user) {
174
- console.error(`Error: User "${localConfig.current_user}" not found in config.toon`);
175
- console.error(`Available users: ${Object.keys(config.users).join(', ')}`);
176
- process.exit(1);
177
- }
178
- return user.email;
179
- }
180
-
181
- export function getLinearClient(): LinearClient {
182
- const apiKey = process.env.LINEAR_API_KEY;
183
- if (!apiKey) {
184
- console.error('Error: LINEAR_API_KEY environment variable is not set.');
185
- console.error('Set it in your shell: export LINEAR_API_KEY="lin_api_xxxxx"');
186
- process.exit(1);
187
- }
188
- return new LinearClient({ apiKey });
189
- }
190
-
191
- export async function loadCycleData(): Promise<CycleData | null> {
192
- try {
193
- await fs.access(CYCLE_PATH);
194
- const fileContent = await fs.readFile(CYCLE_PATH, 'utf-8');
195
- return decode(fileContent) as unknown as CycleData;
196
- } catch {
197
- return null;
198
- }
199
- }
200
-
201
- export async function saveCycleData(data: CycleData): Promise<void> {
202
- const toonString = encode(data);
203
- await fs.writeFile(CYCLE_PATH, toonString, 'utf-8');
204
- }
205
-
206
- export async function saveConfig(config: Config): Promise<void> {
207
- const toonString = encode(config);
208
- await fs.writeFile(CONFIG_PATH, toonString, 'utf-8');
209
- }
210
-
211
- export async function saveLocalConfig(config: LocalConfig): Promise<void> {
212
- const toonString = encode(config);
213
- await fs.writeFile(LOCAL_PATH, toonString, 'utf-8');
214
- }
215
-
216
- // Get first team key from config
217
- export function getDefaultTeamKey(config: Config): string {
218
- const keys = Object.keys(config.teams);
219
- if (keys.length === 0) {
220
- console.error('Error: No teams defined in config.toon');
221
- process.exit(1);
222
- }
223
- return keys[0];
224
- }
225
-
226
- // Get team ID by key or return first team
227
- export function getTeamId(config: Config, teamKey?: string): string {
228
- const key = teamKey || getDefaultTeamKey(config);
229
- const team = config.teams[key];
230
- if (!team) {
231
- console.error(`Error: Team "${key}" not found in config.toon`);
232
- console.error(`Available teams: ${Object.keys(config.teams).join(', ')}`);
233
- process.exit(1);
234
- }
235
- return team.id;
236
- }
@@ -1,138 +0,0 @@
1
- import prompts from 'prompts';
2
- import { getLinearClient, loadConfig, loadLocalConfig, loadCycleData, saveCycleData, getUserEmail, getTeamId, getPrioritySortIndex } from './utils.js';
3
- const PRIORITY_LABELS = {
4
- 0: '⚪ None',
5
- 1: '🔴 Urgent',
6
- 2: '🟠 High',
7
- 3: '🟡 Medium',
8
- 4: '🟢 Low'
9
- };
10
- async function workOn() {
11
- const args = process.argv.slice(2);
12
- // Handle help flag
13
- if (args.includes('--help') || args.includes('-h')) {
14
- console.log(`Usage: ttt work-on [issue-id]
15
-
16
- Arguments:
17
- issue-id Issue ID (e.g., MP-624) or 'next' for auto-select
18
- If omitted, shows interactive selection
19
-
20
- Examples:
21
- ttt work-on # Interactive selection
22
- ttt work-on MP-624 # Work on specific issue
23
- ttt work-on next # Auto-select highest priority`);
24
- process.exit(0);
25
- }
26
- let issueId = args[0];
27
- const config = await loadConfig();
28
- const data = await loadCycleData();
29
- if (!data) {
30
- console.error('No cycle data found. Run /sync-linear first.');
31
- process.exit(1);
32
- }
33
- const userEmail = await getUserEmail();
34
- const localConfig = await loadLocalConfig();
35
- // Build excluded emails list from user keys
36
- const excludedEmails = new Set((localConfig.exclude_assignees ?? [])
37
- .map(key => config.users[key]?.email)
38
- .filter(Boolean));
39
- const pendingTasks = data.tasks
40
- .filter(t => t.localStatus === 'pending' &&
41
- !excludedEmails.has(t.assignee ?? ''))
42
- .sort((a, b) => {
43
- const pa = getPrioritySortIndex(a.priority, config.priority_order);
44
- const pb = getPrioritySortIndex(b.priority, config.priority_order);
45
- return pa - pb;
46
- });
47
- // Phase 0: Issue Resolution
48
- if (!issueId) {
49
- // Interactive selection
50
- if (pendingTasks.length === 0) {
51
- console.log('✅ 沒有待處理的任務,所有工作已完成或進行中');
52
- process.exit(0);
53
- }
54
- const choices = pendingTasks.map(task => ({
55
- title: `${PRIORITY_LABELS[task.priority] || '⚪'} ${task.id}: ${task.title}`,
56
- value: task.id,
57
- description: task.labels.join(', ')
58
- }));
59
- const response = await prompts({
60
- type: 'select',
61
- name: 'issueId',
62
- message: '選擇要處理的任務:',
63
- choices: choices
64
- });
65
- if (!response.issueId) {
66
- console.log('已取消');
67
- process.exit(0);
68
- }
69
- issueId = response.issueId;
70
- }
71
- else if (['next', '下一個', '下一個工作'].includes(issueId)) {
72
- // Auto-select highest priority
73
- if (pendingTasks.length === 0) {
74
- console.log('✅ 沒有待處理的任務,所有工作已完成或進行中');
75
- process.exit(0);
76
- }
77
- issueId = pendingTasks[0].id;
78
- console.log(`Auto-selected: ${issueId}`);
79
- }
80
- // Phase 1: Find task
81
- const task = data.tasks.find(t => t.id === issueId || t.id === `MP-${issueId}`);
82
- if (!task) {
83
- console.error(`Issue ${issueId} not found in current cycle.`);
84
- process.exit(1);
85
- }
86
- // Phase 2: Availability Check
87
- if (task.localStatus === 'in-progress') {
88
- console.log(`⚠️ 此任務 ${task.id} 已在進行中`);
89
- }
90
- else if (task.localStatus === 'completed') {
91
- console.log(`⚠️ 此任務 ${task.id} 已完成`);
92
- process.exit(0);
93
- }
94
- // Phase 3: Mark as In Progress
95
- if (task.localStatus === 'pending') {
96
- task.localStatus = 'in-progress';
97
- await saveCycleData(data);
98
- console.log(`Local: ${task.id} → in-progress`);
99
- // Update Linear using stored linearId
100
- if (task.linearId && process.env.LINEAR_API_KEY) {
101
- try {
102
- const client = getLinearClient();
103
- const workflowStates = await client.workflowStates({
104
- filter: { team: { id: { eq: getTeamId(config, localConfig.team) } } }
105
- });
106
- const inProgressState = workflowStates.nodes.find(s => s.name === 'In Progress');
107
- if (inProgressState) {
108
- await client.updateIssue(task.linearId, { stateId: inProgressState.id });
109
- console.log(`Linear: ${task.id} → In Progress`);
110
- }
111
- }
112
- catch (e) {
113
- console.error('Failed to update Linear:', e);
114
- }
115
- }
116
- }
117
- // Phase 4: Display task info
118
- console.log(`\n${'═'.repeat(50)}`);
119
- console.log(`👷 ${task.id}: ${task.title}`);
120
- console.log(`${'═'.repeat(50)}`);
121
- console.log(`Priority: ${PRIORITY_LABELS[task.priority] || 'None'}`);
122
- console.log(`Labels: ${task.labels.join(', ')}`);
123
- console.log(`Branch: ${task.branch || 'N/A'}`);
124
- if (task.url)
125
- console.log(`URL: ${task.url}`);
126
- if (task.description) {
127
- console.log(`\n📝 Description:\n${task.description}`);
128
- }
129
- if (task.attachments && task.attachments.length > 0) {
130
- console.log(`\n📎 Attachments:`);
131
- for (const att of task.attachments) {
132
- console.log(` - ${att.title}: ${att.url}`);
133
- }
134
- }
135
- console.log(`\n${'─'.repeat(50)}`);
136
- console.log('Next: bun type-check && bun lint');
137
- }
138
- workOn().catch(console.error);