@masslessai/push-todo 3.0.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/.claude-plugin/plugin.json +5 -0
- package/LICENSE +21 -0
- package/README.md +107 -0
- package/SKILL.md +180 -0
- package/bin/push-todo.js +23 -0
- package/commands/push-todo.md +78 -0
- package/hooks/hooks.json +26 -0
- package/hooks/session-end.js +99 -0
- package/hooks/session-start.js +134 -0
- package/lib/api.js +325 -0
- package/lib/cli.js +191 -0
- package/lib/config.js +279 -0
- package/lib/connect.js +380 -0
- package/lib/encryption.js +201 -0
- package/lib/fetch.js +371 -0
- package/lib/index.js +114 -0
- package/lib/machine-id.js +101 -0
- package/lib/project-registry.js +279 -0
- package/lib/utils/colors.js +126 -0
- package/lib/utils/format.js +253 -0
- package/lib/utils/git.js +149 -0
- package/lib/watch.js +343 -0
- package/natives/KeychainHelper.swift +134 -0
- package/package.json +54 -0
- package/scripts/postinstall.js +136 -0
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Machine Identification for Push CLI.
|
|
3
|
+
*
|
|
4
|
+
* Generates and persists a unique machine identifier used for:
|
|
5
|
+
* - Atomic task claiming (prevents multi-Mac race conditions)
|
|
6
|
+
* - Task attribution (which Mac executed a task)
|
|
7
|
+
* - Worktree naming (prevents branch conflicts)
|
|
8
|
+
*
|
|
9
|
+
* File location: ~/.config/push/machine_id
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { readFileSync, writeFileSync, mkdirSync, existsSync } from 'fs';
|
|
13
|
+
import { homedir, hostname, platform, release, version, arch } from 'os';
|
|
14
|
+
import { join, dirname } from 'path';
|
|
15
|
+
import { randomUUID } from 'crypto';
|
|
16
|
+
|
|
17
|
+
const MACHINE_ID_FILE = join(homedir(), '.config', 'push', 'machine_id');
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Get or create a unique machine identifier.
|
|
21
|
+
*
|
|
22
|
+
* Format: "{hostname}-{random_hex}"
|
|
23
|
+
* Example: "Yuxiang-MacBook-Pro-a1b2c3d4"
|
|
24
|
+
*
|
|
25
|
+
* The ID is persisted to disk and reused across sessions.
|
|
26
|
+
*
|
|
27
|
+
* @returns {string} Unique machine identifier
|
|
28
|
+
*/
|
|
29
|
+
export function getMachineId() {
|
|
30
|
+
// Try to read existing ID
|
|
31
|
+
if (existsSync(MACHINE_ID_FILE)) {
|
|
32
|
+
try {
|
|
33
|
+
const storedId = readFileSync(MACHINE_ID_FILE, 'utf8').trim();
|
|
34
|
+
if (storedId) {
|
|
35
|
+
return storedId;
|
|
36
|
+
}
|
|
37
|
+
} catch {
|
|
38
|
+
// Fall through to generate new ID
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Generate new ID: hostname + random suffix
|
|
43
|
+
const host = hostname(); // e.g., "Yuxiang-MacBook-Pro"
|
|
44
|
+
const randomSuffix = randomUUID().slice(0, 8); // e.g., "a1b2c3d4"
|
|
45
|
+
const machineId = `${host}-${randomSuffix}`;
|
|
46
|
+
|
|
47
|
+
// Persist to disk
|
|
48
|
+
try {
|
|
49
|
+
mkdirSync(dirname(MACHINE_ID_FILE), { recursive: true });
|
|
50
|
+
writeFileSync(MACHINE_ID_FILE, machineId);
|
|
51
|
+
} catch {
|
|
52
|
+
// If we can't persist, still return the ID for this session
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return machineId;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Get human-readable machine name.
|
|
60
|
+
*
|
|
61
|
+
* Returns the hostname without the random suffix.
|
|
62
|
+
* Example: "Yuxiang-MacBook-Pro"
|
|
63
|
+
*
|
|
64
|
+
* @returns {string} Human-readable machine name
|
|
65
|
+
*/
|
|
66
|
+
export function getMachineName() {
|
|
67
|
+
return hostname();
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Get full machine information for debugging.
|
|
72
|
+
*
|
|
73
|
+
* @returns {Object} Machine info
|
|
74
|
+
*/
|
|
75
|
+
export function getMachineInfo() {
|
|
76
|
+
return {
|
|
77
|
+
machineId: getMachineId(),
|
|
78
|
+
machineName: getMachineName(),
|
|
79
|
+
platform: platform(), // "darwin" for macOS
|
|
80
|
+
release: release(), // OS kernel version
|
|
81
|
+
version: version(), // OS version string
|
|
82
|
+
arch: arch(), // "arm64" or "x64"
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Delete the stored machine ID (for testing/debugging).
|
|
88
|
+
* The next call to getMachineId() will generate a new ID.
|
|
89
|
+
*/
|
|
90
|
+
export function resetMachineId() {
|
|
91
|
+
if (existsSync(MACHINE_ID_FILE)) {
|
|
92
|
+
try {
|
|
93
|
+
const { unlinkSync } = require('fs');
|
|
94
|
+
unlinkSync(MACHINE_ID_FILE);
|
|
95
|
+
} catch {
|
|
96
|
+
// Ignore errors
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export { MACHINE_ID_FILE };
|
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Project Registry for Push CLI.
|
|
3
|
+
*
|
|
4
|
+
* Maps git_remote to local paths for global daemon routing.
|
|
5
|
+
* Enables the daemon to route tasks to the correct project directory.
|
|
6
|
+
*
|
|
7
|
+
* File location: ~/.config/push/projects.json
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { readFileSync, writeFileSync, mkdirSync, existsSync, statSync } from 'fs';
|
|
11
|
+
import { homedir } from 'os';
|
|
12
|
+
import { join } from 'path';
|
|
13
|
+
|
|
14
|
+
const REGISTRY_FILE = join(homedir(), '.config', 'push', 'projects.json');
|
|
15
|
+
const REGISTRY_VERSION = 1;
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Project Registry class.
|
|
19
|
+
* Manages the local project registry for global daemon routing.
|
|
20
|
+
*/
|
|
21
|
+
class ProjectRegistry {
|
|
22
|
+
constructor() {
|
|
23
|
+
this._ensureConfigDir();
|
|
24
|
+
this._data = this._load();
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
_ensureConfigDir() {
|
|
28
|
+
const dir = join(homedir(), '.config', 'push');
|
|
29
|
+
mkdirSync(dir, { recursive: true });
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
_load() {
|
|
33
|
+
if (!existsSync(REGISTRY_FILE)) {
|
|
34
|
+
return {
|
|
35
|
+
version: REGISTRY_VERSION,
|
|
36
|
+
projects: {},
|
|
37
|
+
defaultProject: null
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
try {
|
|
42
|
+
const content = readFileSync(REGISTRY_FILE, 'utf8');
|
|
43
|
+
const data = JSON.parse(content);
|
|
44
|
+
|
|
45
|
+
// Migration: handle older versions if needed
|
|
46
|
+
if ((data.version || 0) < REGISTRY_VERSION) {
|
|
47
|
+
return this._migrate(data);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return data;
|
|
51
|
+
} catch {
|
|
52
|
+
return {
|
|
53
|
+
version: REGISTRY_VERSION,
|
|
54
|
+
projects: {},
|
|
55
|
+
defaultProject: null
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
_save() {
|
|
61
|
+
writeFileSync(REGISTRY_FILE, JSON.stringify(this._data, null, 2));
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
_migrate(data) {
|
|
65
|
+
// Future: handle migrations as needed
|
|
66
|
+
data.version = REGISTRY_VERSION;
|
|
67
|
+
return data;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Register a project.
|
|
72
|
+
*
|
|
73
|
+
* @param {string} gitRemote - Normalized git remote (e.g., "github.com/user/repo")
|
|
74
|
+
* @param {string} localPath - Absolute local path
|
|
75
|
+
* @returns {boolean} True if newly registered, false if updated existing
|
|
76
|
+
*/
|
|
77
|
+
register(gitRemote, localPath) {
|
|
78
|
+
const isNew = !(gitRemote in this._data.projects);
|
|
79
|
+
const now = new Date().toISOString();
|
|
80
|
+
|
|
81
|
+
if (isNew) {
|
|
82
|
+
this._data.projects[gitRemote] = {
|
|
83
|
+
localPath,
|
|
84
|
+
registeredAt: now,
|
|
85
|
+
lastUsed: now
|
|
86
|
+
};
|
|
87
|
+
} else {
|
|
88
|
+
this._data.projects[gitRemote].localPath = localPath;
|
|
89
|
+
this._data.projects[gitRemote].lastUsed = now;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Set as default if first project
|
|
93
|
+
if (this._data.defaultProject === null) {
|
|
94
|
+
this._data.defaultProject = gitRemote;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
this._save();
|
|
98
|
+
return isNew;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Get local path for a git remote.
|
|
103
|
+
* Updates lastUsed timestamp.
|
|
104
|
+
*
|
|
105
|
+
* @param {string} gitRemote - Normalized git remote
|
|
106
|
+
* @returns {string|null} Local path or null if not registered
|
|
107
|
+
*/
|
|
108
|
+
getPath(gitRemote) {
|
|
109
|
+
const project = this._data.projects[gitRemote];
|
|
110
|
+
if (project) {
|
|
111
|
+
// Update last_used
|
|
112
|
+
project.lastUsed = new Date().toISOString();
|
|
113
|
+
this._save();
|
|
114
|
+
return project.localPath;
|
|
115
|
+
}
|
|
116
|
+
return null;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Get local path without updating lastUsed.
|
|
121
|
+
* Useful for status checks and listing operations.
|
|
122
|
+
*
|
|
123
|
+
* @param {string} gitRemote - Normalized git remote
|
|
124
|
+
* @returns {string|null} Local path or null if not registered
|
|
125
|
+
*/
|
|
126
|
+
getPathWithoutUpdate(gitRemote) {
|
|
127
|
+
const project = this._data.projects[gitRemote];
|
|
128
|
+
return project ? project.localPath : null;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* List all registered projects.
|
|
133
|
+
*
|
|
134
|
+
* @returns {Object} Dict of gitRemote -> localPath
|
|
135
|
+
*/
|
|
136
|
+
listProjects() {
|
|
137
|
+
const result = {};
|
|
138
|
+
for (const [remote, info] of Object.entries(this._data.projects)) {
|
|
139
|
+
result[remote] = info.localPath;
|
|
140
|
+
}
|
|
141
|
+
return result;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* List all registered projects with full metadata.
|
|
146
|
+
*
|
|
147
|
+
* @returns {Object} Dict of gitRemote -> {localPath, registeredAt, lastUsed}
|
|
148
|
+
*/
|
|
149
|
+
listProjectsWithMetadata() {
|
|
150
|
+
return { ...this._data.projects };
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Unregister a project.
|
|
155
|
+
*
|
|
156
|
+
* @param {string} gitRemote - Normalized git remote
|
|
157
|
+
* @returns {boolean} True if was registered, false if not found
|
|
158
|
+
*/
|
|
159
|
+
unregister(gitRemote) {
|
|
160
|
+
if (gitRemote in this._data.projects) {
|
|
161
|
+
delete this._data.projects[gitRemote];
|
|
162
|
+
|
|
163
|
+
if (this._data.defaultProject === gitRemote) {
|
|
164
|
+
// Set new default
|
|
165
|
+
const remaining = Object.keys(this._data.projects);
|
|
166
|
+
this._data.defaultProject = remaining.length > 0 ? remaining[0] : null;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
this._save();
|
|
170
|
+
return true;
|
|
171
|
+
}
|
|
172
|
+
return false;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Get the default project's git remote.
|
|
177
|
+
*
|
|
178
|
+
* @returns {string|null}
|
|
179
|
+
*/
|
|
180
|
+
getDefaultProject() {
|
|
181
|
+
return this._data.defaultProject;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Set a project as the default.
|
|
186
|
+
*
|
|
187
|
+
* @param {string} gitRemote
|
|
188
|
+
* @returns {boolean} True if successful
|
|
189
|
+
*/
|
|
190
|
+
setDefaultProject(gitRemote) {
|
|
191
|
+
if (gitRemote in this._data.projects) {
|
|
192
|
+
this._data.defaultProject = gitRemote;
|
|
193
|
+
this._save();
|
|
194
|
+
return true;
|
|
195
|
+
}
|
|
196
|
+
return false;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Return the number of registered projects.
|
|
201
|
+
*
|
|
202
|
+
* @returns {number}
|
|
203
|
+
*/
|
|
204
|
+
projectCount() {
|
|
205
|
+
return Object.keys(this._data.projects).length;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Check if a project is registered.
|
|
210
|
+
*
|
|
211
|
+
* @param {string} gitRemote
|
|
212
|
+
* @returns {boolean}
|
|
213
|
+
*/
|
|
214
|
+
isRegistered(gitRemote) {
|
|
215
|
+
return gitRemote in this._data.projects;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Validate that all registered paths still exist.
|
|
220
|
+
*
|
|
221
|
+
* @returns {Array} List of invalid entries
|
|
222
|
+
*/
|
|
223
|
+
validatePaths() {
|
|
224
|
+
const invalid = [];
|
|
225
|
+
|
|
226
|
+
for (const [gitRemote, info] of Object.entries(this._data.projects)) {
|
|
227
|
+
const path = info.localPath;
|
|
228
|
+
|
|
229
|
+
try {
|
|
230
|
+
const stats = statSync(path);
|
|
231
|
+
if (!stats.isDirectory()) {
|
|
232
|
+
invalid.push({
|
|
233
|
+
gitRemote,
|
|
234
|
+
localPath: path,
|
|
235
|
+
reason: 'not_a_directory'
|
|
236
|
+
});
|
|
237
|
+
} else if (!existsSync(join(path, '.git'))) {
|
|
238
|
+
invalid.push({
|
|
239
|
+
gitRemote,
|
|
240
|
+
localPath: path,
|
|
241
|
+
reason: 'not_a_git_repo'
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
} catch {
|
|
245
|
+
invalid.push({
|
|
246
|
+
gitRemote,
|
|
247
|
+
localPath: path,
|
|
248
|
+
reason: 'path_not_found'
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
return invalid;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// Singleton instance
|
|
258
|
+
let _registry = null;
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* Get the singleton registry instance.
|
|
262
|
+
*
|
|
263
|
+
* @returns {ProjectRegistry}
|
|
264
|
+
*/
|
|
265
|
+
export function getRegistry() {
|
|
266
|
+
if (_registry === null) {
|
|
267
|
+
_registry = new ProjectRegistry();
|
|
268
|
+
}
|
|
269
|
+
return _registry;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* Reset the singleton (for testing).
|
|
274
|
+
*/
|
|
275
|
+
export function resetRegistry() {
|
|
276
|
+
_registry = null;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
export { ProjectRegistry, REGISTRY_FILE };
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Terminal color utilities for Push CLI.
|
|
3
|
+
*
|
|
4
|
+
* ANSI escape codes for terminal styling.
|
|
5
|
+
* Automatically disables colors when not outputting to a TTY.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const isColorEnabled = process.stdout.isTTY && !process.env.NO_COLOR;
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* ANSI escape codes.
|
|
12
|
+
*/
|
|
13
|
+
export const codes = {
|
|
14
|
+
// Reset
|
|
15
|
+
reset: '\x1b[0m',
|
|
16
|
+
|
|
17
|
+
// Styles
|
|
18
|
+
bold: '\x1b[1m',
|
|
19
|
+
dim: '\x1b[2m',
|
|
20
|
+
italic: '\x1b[3m',
|
|
21
|
+
underline: '\x1b[4m',
|
|
22
|
+
|
|
23
|
+
// Colors
|
|
24
|
+
black: '\x1b[30m',
|
|
25
|
+
red: '\x1b[31m',
|
|
26
|
+
green: '\x1b[32m',
|
|
27
|
+
yellow: '\x1b[33m',
|
|
28
|
+
blue: '\x1b[34m',
|
|
29
|
+
magenta: '\x1b[35m',
|
|
30
|
+
cyan: '\x1b[36m',
|
|
31
|
+
white: '\x1b[37m',
|
|
32
|
+
|
|
33
|
+
// Bright colors
|
|
34
|
+
brightBlack: '\x1b[90m',
|
|
35
|
+
brightRed: '\x1b[91m',
|
|
36
|
+
brightGreen: '\x1b[92m',
|
|
37
|
+
brightYellow: '\x1b[93m',
|
|
38
|
+
brightBlue: '\x1b[94m',
|
|
39
|
+
brightMagenta: '\x1b[95m',
|
|
40
|
+
brightCyan: '\x1b[96m',
|
|
41
|
+
brightWhite: '\x1b[97m',
|
|
42
|
+
|
|
43
|
+
// Background colors
|
|
44
|
+
bgBlack: '\x1b[40m',
|
|
45
|
+
bgRed: '\x1b[41m',
|
|
46
|
+
bgGreen: '\x1b[42m',
|
|
47
|
+
bgYellow: '\x1b[43m',
|
|
48
|
+
bgBlue: '\x1b[44m',
|
|
49
|
+
bgMagenta: '\x1b[45m',
|
|
50
|
+
bgCyan: '\x1b[46m',
|
|
51
|
+
bgWhite: '\x1b[47m',
|
|
52
|
+
|
|
53
|
+
// Cursor control
|
|
54
|
+
clearScreen: '\x1b[2J',
|
|
55
|
+
cursorHome: '\x1b[H',
|
|
56
|
+
hideCursor: '\x1b[?25l',
|
|
57
|
+
showCursor: '\x1b[?25h',
|
|
58
|
+
clearLine: '\x1b[2K',
|
|
59
|
+
cursorUp: '\x1b[1A',
|
|
60
|
+
cursorDown: '\x1b[1B',
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Wrap text with color codes.
|
|
65
|
+
*
|
|
66
|
+
* @param {string} text - Text to color
|
|
67
|
+
* @param {string} code - ANSI code
|
|
68
|
+
* @returns {string} Colored text (or plain text if colors disabled)
|
|
69
|
+
*/
|
|
70
|
+
function wrap(text, code) {
|
|
71
|
+
if (!isColorEnabled) {
|
|
72
|
+
return text;
|
|
73
|
+
}
|
|
74
|
+
return `${code}${text}${codes.reset}`;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Style functions
|
|
78
|
+
export const bold = (text) => wrap(text, codes.bold);
|
|
79
|
+
export const dim = (text) => wrap(text, codes.dim);
|
|
80
|
+
export const italic = (text) => wrap(text, codes.italic);
|
|
81
|
+
export const underline = (text) => wrap(text, codes.underline);
|
|
82
|
+
|
|
83
|
+
// Color functions
|
|
84
|
+
export const red = (text) => wrap(text, codes.red);
|
|
85
|
+
export const green = (text) => wrap(text, codes.green);
|
|
86
|
+
export const yellow = (text) => wrap(text, codes.yellow);
|
|
87
|
+
export const blue = (text) => wrap(text, codes.blue);
|
|
88
|
+
export const magenta = (text) => wrap(text, codes.magenta);
|
|
89
|
+
export const cyan = (text) => wrap(text, codes.cyan);
|
|
90
|
+
export const white = (text) => wrap(text, codes.white);
|
|
91
|
+
|
|
92
|
+
// Bright color functions
|
|
93
|
+
export const brightRed = (text) => wrap(text, codes.brightRed);
|
|
94
|
+
export const brightGreen = (text) => wrap(text, codes.brightGreen);
|
|
95
|
+
export const brightYellow = (text) => wrap(text, codes.brightYellow);
|
|
96
|
+
export const brightBlue = (text) => wrap(text, codes.brightBlue);
|
|
97
|
+
export const brightCyan = (text) => wrap(text, codes.brightCyan);
|
|
98
|
+
|
|
99
|
+
// Combined styles
|
|
100
|
+
export const error = (text) => wrap(text, codes.red);
|
|
101
|
+
export const success = (text) => wrap(text, codes.green);
|
|
102
|
+
export const warning = (text) => wrap(text, codes.yellow);
|
|
103
|
+
export const info = (text) => wrap(text, codes.cyan);
|
|
104
|
+
export const muted = (text) => wrap(text, codes.dim);
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Status indicator symbols with colors.
|
|
108
|
+
*/
|
|
109
|
+
export const symbols = {
|
|
110
|
+
running: isColorEnabled ? `${codes.green}●${codes.reset}` : '*',
|
|
111
|
+
queued: isColorEnabled ? `${codes.yellow}○${codes.reset}` : 'o',
|
|
112
|
+
completed: isColorEnabled ? `${codes.green}✓${codes.reset}` : '+',
|
|
113
|
+
failed: isColorEnabled ? `${codes.red}✗${codes.reset}` : 'x',
|
|
114
|
+
timeout: isColorEnabled ? `${codes.red}⏱${codes.reset}` : 'T',
|
|
115
|
+
backlog: isColorEnabled ? `${codes.blue}📦${codes.reset}` : '[B]',
|
|
116
|
+
pinned: isColorEnabled ? `${codes.yellow}📌${codes.reset}` : '[P]',
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Check if colors are enabled.
|
|
121
|
+
*
|
|
122
|
+
* @returns {boolean}
|
|
123
|
+
*/
|
|
124
|
+
export function colorsEnabled() {
|
|
125
|
+
return isColorEnabled;
|
|
126
|
+
}
|