taskin 4.1.0 → 4.1.2
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/dist/index.js
CHANGED
|
@@ -28,436 +28,868 @@ var init_esm_shims = __esm({
|
|
|
28
28
|
}
|
|
29
29
|
});
|
|
30
30
|
|
|
31
|
-
// ../utils/src/
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
return
|
|
35
|
-
}
|
|
36
|
-
function
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
31
|
+
// ../git-utils/src/commit-message.ts
|
|
32
|
+
function isRecognizedCiSkipTag(tag) {
|
|
33
|
+
const normalized = tag.trim().toLowerCase();
|
|
34
|
+
return CI_SKIP_TAGS.some((known) => known === normalized);
|
|
35
|
+
}
|
|
36
|
+
function appendCiSkipTag(subject, ciSkipTag = DEFAULT_CI_SKIP_TAG) {
|
|
37
|
+
const tag = ciSkipTag.trim();
|
|
38
|
+
if (tag.length === 0) return subject;
|
|
39
|
+
if (subject.endsWith(tag)) return subject;
|
|
40
|
+
return `${subject} ${tag}`;
|
|
41
|
+
}
|
|
42
|
+
function buildTaskStatusCommitMessage(options) {
|
|
43
|
+
const subject = `docs(TASKS): task-${options.taskId} - atualiza status para ${options.status}`;
|
|
44
|
+
return appendCiSkipTag(subject, options.ciSkipTag);
|
|
45
|
+
}
|
|
46
|
+
var CI_SKIP_TAGS, DEFAULT_CI_SKIP_TAG;
|
|
47
|
+
var init_commit_message = __esm({
|
|
48
|
+
"../git-utils/src/commit-message.ts"() {
|
|
49
|
+
"use strict";
|
|
50
|
+
init_esm_shims();
|
|
51
|
+
CI_SKIP_TAGS = ["[skip ci]", "[ci skip]", "[no ci]", "[skip actions]", "[actions skip]"];
|
|
52
|
+
DEFAULT_CI_SKIP_TAG = "[skip ci]";
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
// ../git-utils/src/git.ts
|
|
57
|
+
import { execSync } from "child_process";
|
|
58
|
+
function executeGit(command) {
|
|
59
|
+
try {
|
|
60
|
+
return execSync(`git ${command}`, {
|
|
61
|
+
encoding: "utf8",
|
|
62
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
63
|
+
}).trim();
|
|
64
|
+
} catch {
|
|
41
65
|
return "";
|
|
42
66
|
}
|
|
43
|
-
const htmlEscapeMap = {
|
|
44
|
-
"&": "&",
|
|
45
|
-
"<": "<",
|
|
46
|
-
">": ">",
|
|
47
|
-
'"': """,
|
|
48
|
-
"'": "'",
|
|
49
|
-
"/": "/"
|
|
50
|
-
};
|
|
51
|
-
return text.replace(/[&<>"'/]/g, (char) => htmlEscapeMap[char]);
|
|
52
67
|
}
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
68
|
+
function isGitRepository() {
|
|
69
|
+
return !!executeGit("rev-parse --is-inside-work-tree");
|
|
70
|
+
}
|
|
71
|
+
function getCurrentBranch() {
|
|
72
|
+
return executeGit("branch --show-current");
|
|
73
|
+
}
|
|
74
|
+
function createBranch(branchName, baseBranch) {
|
|
75
|
+
const base = baseBranch || getCurrentBranch();
|
|
76
|
+
executeGit(`checkout -b ${branchName} ${base}`);
|
|
77
|
+
}
|
|
78
|
+
var init_git = __esm({
|
|
79
|
+
"../git-utils/src/git.ts"() {
|
|
56
80
|
"use strict";
|
|
57
81
|
init_esm_shims();
|
|
58
|
-
HostSchema = z.string().refine(
|
|
59
|
-
(host) => {
|
|
60
|
-
if (!host || host.length === 0) return false;
|
|
61
|
-
if (host === "localhost") return true;
|
|
62
|
-
const parts = host.split(".");
|
|
63
|
-
const allNumeric = parts.every((p) => /^\d+$/.test(p));
|
|
64
|
-
if (allNumeric) {
|
|
65
|
-
if (parts.length !== 4) return false;
|
|
66
|
-
return parts.every((part) => {
|
|
67
|
-
const num = parseInt(part, 10);
|
|
68
|
-
return !Number.isNaN(num) && num >= 0 && num <= 255 && part === num.toString();
|
|
69
|
-
});
|
|
70
|
-
}
|
|
71
|
-
const hostnameRegex = /^[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(\.[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/;
|
|
72
|
-
return hostnameRegex.test(host);
|
|
73
|
-
},
|
|
74
|
-
{
|
|
75
|
-
message: "Invalid host. Must be localhost, a valid IPv4 address, or hostname."
|
|
76
|
-
}
|
|
77
|
-
);
|
|
78
|
-
PortSchema = z.union([
|
|
79
|
-
z.number().int().min(1).max(65535),
|
|
80
|
-
z.string().regex(/^\d+$/).transform((val) => parseInt(val, 10)).pipe(z.number().int().min(1).max(65535))
|
|
81
|
-
]);
|
|
82
|
-
WebSocketUrlSchema = z.string().refine(
|
|
83
|
-
(url) => {
|
|
84
|
-
try {
|
|
85
|
-
const parsed = new URL(url);
|
|
86
|
-
return parsed.protocol === "ws:" || parsed.protocol === "wss:";
|
|
87
|
-
} catch {
|
|
88
|
-
return false;
|
|
89
|
-
}
|
|
90
|
-
},
|
|
91
|
-
{ message: "Invalid WebSocket URL. Must use ws:// or wss:// protocol." }
|
|
92
|
-
);
|
|
93
|
-
TaskIdSchema2 = z.string().min(1).max(100).regex(/^[a-zA-Z0-9_-]+$/, {
|
|
94
|
-
message: "Task ID must contain only alphanumeric characters, hyphens, and underscores."
|
|
95
|
-
});
|
|
96
|
-
UserIdSchema = z.string().min(1).max(100).regex(/^[a-zA-Z0-9._-]+$/, {
|
|
97
|
-
message: "User ID must contain only alphanumeric characters, dots, hyphens, and underscores."
|
|
98
|
-
});
|
|
99
|
-
EmailSchema = z.string().email().max(254);
|
|
100
|
-
SafePathSchema = z.string().refine(
|
|
101
|
-
(filePath) => {
|
|
102
|
-
if (!filePath || filePath.length === 0) return false;
|
|
103
|
-
const dangerousPatterns = [
|
|
104
|
-
/\.\./,
|
|
105
|
-
// Parent directory (..)
|
|
106
|
-
/~\//,
|
|
107
|
-
// Home directory
|
|
108
|
-
/^\//,
|
|
109
|
-
// Absolute path
|
|
110
|
-
/^[A-Za-z]:\\/
|
|
111
|
-
// Windows absolute path
|
|
112
|
-
];
|
|
113
|
-
return !dangerousPatterns.some((pattern) => pattern.test(filePath));
|
|
114
|
-
},
|
|
115
|
-
{
|
|
116
|
-
message: "Invalid path. Must be a relative path without traversal patterns."
|
|
117
|
-
}
|
|
118
|
-
);
|
|
119
|
-
DashboardOptionsSchema = z.object({
|
|
120
|
-
host: HostSchema.optional(),
|
|
121
|
-
port: PortSchema.optional(),
|
|
122
|
-
wsPort: PortSchema.optional()
|
|
123
|
-
});
|
|
124
82
|
}
|
|
125
83
|
});
|
|
126
84
|
|
|
127
|
-
// ../utils/src/
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
}
|
|
131
|
-
var init_string = __esm({
|
|
132
|
-
"../utils/src/string.ts"() {
|
|
85
|
+
// ../git-utils/src/git.types.ts
|
|
86
|
+
var init_git_types = __esm({
|
|
87
|
+
"../git-utils/src/git.types.ts"() {
|
|
133
88
|
"use strict";
|
|
134
89
|
init_esm_shims();
|
|
135
90
|
}
|
|
136
91
|
});
|
|
137
92
|
|
|
138
|
-
// ../utils/src/
|
|
139
|
-
import
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
"
|
|
93
|
+
// ../git-utils/src/git-analyzer.ts
|
|
94
|
+
import { exec } from "child_process";
|
|
95
|
+
import { promisify } from "util";
|
|
96
|
+
function isNodeError(error2) {
|
|
97
|
+
return typeof error2 === "object" && error2 !== null && "code" in error2;
|
|
98
|
+
}
|
|
99
|
+
async function executeGit2(command, cwd) {
|
|
100
|
+
try {
|
|
101
|
+
const { stdout } = await execAsync(`git ${command}`, {
|
|
102
|
+
cwd: cwd || process.cwd(),
|
|
103
|
+
encoding: "utf8",
|
|
104
|
+
maxBuffer: 10 * 1024 * 1024,
|
|
105
|
+
// 10MB buffer for large repos
|
|
106
|
+
timeout: 3e4
|
|
107
|
+
// 30 second timeout
|
|
108
|
+
});
|
|
109
|
+
return stdout.trim();
|
|
110
|
+
} catch (error2) {
|
|
111
|
+
if (isNodeError(error2)) {
|
|
112
|
+
if (error2.code === "ENOENT") {
|
|
113
|
+
throw new Error("Git is not installed or not in PATH");
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
return "";
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
var execAsync, GitAnalyzer;
|
|
120
|
+
var init_git_analyzer = __esm({
|
|
121
|
+
"../git-utils/src/git-analyzer.ts"() {
|
|
143
122
|
"use strict";
|
|
144
123
|
init_esm_shims();
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
124
|
+
execAsync = promisify(exec);
|
|
125
|
+
GitAnalyzer = class {
|
|
126
|
+
constructor(repositoryPath) {
|
|
127
|
+
this.repositoryPath = repositoryPath;
|
|
128
|
+
}
|
|
129
|
+
async isValidRepository() {
|
|
130
|
+
try {
|
|
131
|
+
const result = await executeGit2("rev-parse --is-inside-work-tree", this.repositoryPath);
|
|
132
|
+
return result === "true";
|
|
133
|
+
} catch {
|
|
134
|
+
return false;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
async getRepositoryRoot() {
|
|
138
|
+
const result = await executeGit2("rev-parse --show-toplevel", this.repositoryPath);
|
|
139
|
+
if (!result) {
|
|
140
|
+
throw new Error("Not a git repository");
|
|
141
|
+
}
|
|
142
|
+
return result;
|
|
143
|
+
}
|
|
144
|
+
async getCommits(options = {}) {
|
|
145
|
+
const args = ["log"];
|
|
146
|
+
args.push(
|
|
147
|
+
"--pretty='format:%H|%an|%aI|%s%x00%b'",
|
|
148
|
+
"--numstat"
|
|
149
|
+
// Get file stats
|
|
150
|
+
);
|
|
151
|
+
if (options.since) {
|
|
152
|
+
args.push(`--since="${options.since}"`);
|
|
153
|
+
}
|
|
154
|
+
if (options.until) {
|
|
155
|
+
args.push(`--until=${options.until}`);
|
|
156
|
+
}
|
|
157
|
+
if (options.author) {
|
|
158
|
+
args.push(`--author='${options.author}'`);
|
|
159
|
+
}
|
|
160
|
+
if (options.maxCount) {
|
|
161
|
+
args.push(`-n ${options.maxCount}`);
|
|
162
|
+
}
|
|
163
|
+
if (!options.includeMerges) {
|
|
164
|
+
args.push("--no-merges");
|
|
165
|
+
}
|
|
166
|
+
if (options.filePath) {
|
|
167
|
+
args.push("--", options.filePath);
|
|
168
|
+
}
|
|
169
|
+
const command = args.join(" ");
|
|
170
|
+
const output = await executeGit2(command, this.repositoryPath);
|
|
171
|
+
if (!output) {
|
|
172
|
+
return [];
|
|
173
|
+
}
|
|
174
|
+
return this.parseCommits(output);
|
|
175
|
+
}
|
|
176
|
+
parseCommits(output) {
|
|
177
|
+
const commits = [];
|
|
178
|
+
const lines = output.split("\n");
|
|
179
|
+
let i = 0;
|
|
180
|
+
while (i < lines.length) {
|
|
181
|
+
const line = lines[i];
|
|
182
|
+
if (line === void 0) break;
|
|
183
|
+
if (!line.trim()) {
|
|
184
|
+
i++;
|
|
185
|
+
continue;
|
|
186
|
+
}
|
|
187
|
+
if (line.includes("|")) {
|
|
188
|
+
const [hash, author, date, messageWithBody] = line.split("|");
|
|
189
|
+
if (hash === void 0 || author === void 0 || date === void 0 || messageWithBody === void 0) {
|
|
190
|
+
i++;
|
|
191
|
+
continue;
|
|
192
|
+
}
|
|
193
|
+
if (!/^[0-9a-f]{6,40}$/i.test(hash)) {
|
|
194
|
+
i++;
|
|
195
|
+
continue;
|
|
196
|
+
}
|
|
197
|
+
const nullByteIndex = messageWithBody.indexOf("\0");
|
|
198
|
+
const subject = nullByteIndex !== -1 ? messageWithBody.substring(0, nullByteIndex) : messageWithBody;
|
|
199
|
+
const bodyLines = [];
|
|
200
|
+
if (nullByteIndex !== -1) {
|
|
201
|
+
bodyLines.push(messageWithBody.substring(nullByteIndex + 1));
|
|
202
|
+
}
|
|
203
|
+
i++;
|
|
204
|
+
while (i < lines.length) {
|
|
205
|
+
const bodyLine = lines[i];
|
|
206
|
+
if (bodyLine === void 0 || bodyLine.includes("|") || bodyLine.includes(" ")) break;
|
|
207
|
+
if (bodyLine.trim()) {
|
|
208
|
+
bodyLines.push(bodyLine);
|
|
209
|
+
}
|
|
210
|
+
i++;
|
|
211
|
+
}
|
|
212
|
+
const body = bodyLines.join("\n");
|
|
213
|
+
const fullMessage = body ? `${subject}
|
|
214
|
+
|
|
215
|
+
${body}` : subject;
|
|
216
|
+
let filesChanged = 0;
|
|
217
|
+
let linesAdded = 0;
|
|
218
|
+
let linesRemoved = 0;
|
|
219
|
+
while (i < lines.length) {
|
|
220
|
+
const rawStatLine = lines[i];
|
|
221
|
+
if (rawStatLine === void 0 || rawStatLine.includes("|")) break;
|
|
222
|
+
const statLine = rawStatLine.trim();
|
|
223
|
+
if (!statLine) {
|
|
224
|
+
i++;
|
|
225
|
+
continue;
|
|
226
|
+
}
|
|
227
|
+
const [added, removed] = statLine.split(" ");
|
|
228
|
+
if (added !== "-" && removed !== "-") {
|
|
229
|
+
linesAdded += parseInt(added || "0", 10);
|
|
230
|
+
linesRemoved += parseInt(removed || "0", 10);
|
|
231
|
+
filesChanged++;
|
|
232
|
+
}
|
|
233
|
+
i++;
|
|
234
|
+
}
|
|
235
|
+
commits.push({
|
|
236
|
+
hash,
|
|
237
|
+
author,
|
|
238
|
+
date,
|
|
239
|
+
message: subject,
|
|
240
|
+
filesChanged,
|
|
241
|
+
linesAdded,
|
|
242
|
+
linesRemoved,
|
|
243
|
+
coAuthors: this.extractCoAuthors(fullMessage)
|
|
244
|
+
});
|
|
245
|
+
} else {
|
|
246
|
+
i++;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
return commits;
|
|
250
|
+
}
|
|
251
|
+
extractCoAuthors(message) {
|
|
252
|
+
const coAuthorRegex = /Co-authored-by:\s*(.+?)\s*<(.+?)>/gi;
|
|
253
|
+
const matches = Array.from(message.matchAll(coAuthorRegex));
|
|
254
|
+
const names = matches.flatMap((match) => match[1]?.trim() ?? []);
|
|
255
|
+
return names.length > 0 ? names : void 0;
|
|
256
|
+
}
|
|
257
|
+
async getDiff(from = "HEAD", to = "") {
|
|
258
|
+
const args = ["diff", "--numstat"];
|
|
259
|
+
if (to) {
|
|
260
|
+
args.push(`${from}..${to}`);
|
|
261
|
+
} else {
|
|
262
|
+
args.push(from);
|
|
263
|
+
}
|
|
264
|
+
const output = await executeGit2(args.join(" "), this.repositoryPath);
|
|
265
|
+
return this.parseDiff(output);
|
|
266
|
+
}
|
|
267
|
+
parseDiff(output) {
|
|
268
|
+
const files = [];
|
|
269
|
+
let totalLinesAdded = 0;
|
|
270
|
+
let totalLinesRemoved = 0;
|
|
271
|
+
if (!output) {
|
|
272
|
+
return {
|
|
273
|
+
files,
|
|
274
|
+
totalLinesAdded,
|
|
275
|
+
totalLinesRemoved,
|
|
276
|
+
netChange: 0
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
const lines = output.split("\n").filter((l) => l.trim());
|
|
280
|
+
for (const line of lines) {
|
|
281
|
+
const [added, removed, path12] = line.split(" ");
|
|
282
|
+
if (added === void 0 || removed === void 0 || path12 === void 0) continue;
|
|
283
|
+
if (added === "-" || removed === "-") {
|
|
284
|
+
continue;
|
|
285
|
+
}
|
|
286
|
+
const linesAdded = parseInt(added, 10);
|
|
287
|
+
const linesRemoved = parseInt(removed, 10);
|
|
288
|
+
totalLinesAdded += linesAdded;
|
|
289
|
+
totalLinesRemoved += linesRemoved;
|
|
290
|
+
files.push({
|
|
291
|
+
path: path12,
|
|
292
|
+
linesAdded,
|
|
293
|
+
linesRemoved,
|
|
294
|
+
changeType: "modified"
|
|
295
|
+
// Simplified - could be enhanced with --name-status
|
|
296
|
+
});
|
|
297
|
+
}
|
|
298
|
+
return {
|
|
299
|
+
files,
|
|
300
|
+
totalLinesAdded,
|
|
301
|
+
totalLinesRemoved,
|
|
302
|
+
netChange: totalLinesAdded - totalLinesRemoved
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
async getFileDiff(filePath, from = "HEAD", to = "") {
|
|
306
|
+
const args = ["diff", "--numstat"];
|
|
307
|
+
if (to) {
|
|
308
|
+
args.push(`${from}..${to}`);
|
|
309
|
+
} else {
|
|
310
|
+
args.push(from);
|
|
311
|
+
}
|
|
312
|
+
args.push("--", filePath);
|
|
313
|
+
const output = await executeGit2(args.join(" "), this.repositoryPath);
|
|
314
|
+
if (!output) {
|
|
315
|
+
return null;
|
|
316
|
+
}
|
|
317
|
+
const [added, removed, path12] = output.split(" ");
|
|
318
|
+
if (added === void 0 || removed === void 0 || path12 === void 0) {
|
|
319
|
+
return null;
|
|
320
|
+
}
|
|
321
|
+
if (added === "-" || removed === "-") {
|
|
322
|
+
return null;
|
|
323
|
+
}
|
|
324
|
+
return {
|
|
325
|
+
path: path12,
|
|
326
|
+
linesAdded: parseInt(added, 10),
|
|
327
|
+
linesRemoved: parseInt(removed, 10),
|
|
328
|
+
changeType: "modified"
|
|
329
|
+
};
|
|
330
|
+
}
|
|
331
|
+
async getBlame(filePath) {
|
|
332
|
+
const args = ["blame", "--line-porcelain", filePath];
|
|
333
|
+
const output = await executeGit2(args.join(" "), this.repositoryPath);
|
|
334
|
+
if (!output) {
|
|
335
|
+
return [];
|
|
336
|
+
}
|
|
337
|
+
return this.parseBlame(output);
|
|
338
|
+
}
|
|
339
|
+
parseBlame(output) {
|
|
340
|
+
const lines = output.split("\n");
|
|
341
|
+
const blameInfo = [];
|
|
342
|
+
let currentHash = "";
|
|
343
|
+
let currentAuthor = "";
|
|
344
|
+
let currentDate = "";
|
|
345
|
+
let lineNumber = 0;
|
|
346
|
+
for (const line of lines) {
|
|
347
|
+
if (line.match(/^[0-9a-f]{6,40}/i)) {
|
|
348
|
+
const [hash, , finalLine] = line.split(" ");
|
|
349
|
+
currentHash = hash ?? "";
|
|
350
|
+
lineNumber = parseInt(finalLine ?? "", 10);
|
|
351
|
+
} else if (line.startsWith("author ")) {
|
|
352
|
+
currentAuthor = line.substring(7);
|
|
353
|
+
} else if (line.startsWith("author-time ")) {
|
|
354
|
+
const timestamp = parseInt(line.substring(12), 10);
|
|
355
|
+
currentDate = new Date(timestamp * 1e3).toISOString();
|
|
356
|
+
} else if (line.startsWith(" ")) {
|
|
357
|
+
const content = line.substring(1);
|
|
358
|
+
blameInfo.push({
|
|
359
|
+
lineNumber,
|
|
360
|
+
commitHash: currentHash,
|
|
361
|
+
author: currentAuthor,
|
|
362
|
+
date: new Date(currentDate),
|
|
363
|
+
content
|
|
364
|
+
});
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
return blameInfo;
|
|
368
|
+
}
|
|
369
|
+
async getAuthors(options = {}) {
|
|
370
|
+
const args = ["shortlog", "-sne"];
|
|
371
|
+
if (options.since) {
|
|
372
|
+
args.push(`--since=${options.since}`);
|
|
373
|
+
}
|
|
374
|
+
if (options.until) {
|
|
375
|
+
args.push(`--until=${options.until}`);
|
|
376
|
+
}
|
|
377
|
+
if (!options.includeMerges) {
|
|
378
|
+
args.push("--no-merges");
|
|
379
|
+
}
|
|
380
|
+
if (options.filePath) {
|
|
381
|
+
args.push("--", options.filePath);
|
|
382
|
+
}
|
|
383
|
+
const command = args.join(" ");
|
|
384
|
+
const output = await executeGit2(command, this.repositoryPath);
|
|
385
|
+
if (!output) {
|
|
386
|
+
return [];
|
|
387
|
+
}
|
|
388
|
+
return this.parseAuthors(output);
|
|
389
|
+
}
|
|
390
|
+
parseAuthors(output) {
|
|
391
|
+
const lines = output.split("\n").filter((l) => l.trim());
|
|
392
|
+
const authors = [];
|
|
393
|
+
for (const line of lines) {
|
|
394
|
+
const match = line.match(/^\s*(\d+)\s+(.+?)\s+<(.+?)>/);
|
|
395
|
+
if (match) {
|
|
396
|
+
const [, commits, name, email] = match;
|
|
397
|
+
if (commits === void 0 || name === void 0 || email === void 0) continue;
|
|
398
|
+
authors.push({
|
|
399
|
+
name,
|
|
400
|
+
email,
|
|
401
|
+
commits: parseInt(commits, 10)
|
|
402
|
+
});
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
return authors;
|
|
406
|
+
}
|
|
407
|
+
async getFileHistory(filePath, options = {}) {
|
|
408
|
+
return this.getCommits({
|
|
409
|
+
...options,
|
|
410
|
+
filePath
|
|
411
|
+
});
|
|
412
|
+
}
|
|
154
413
|
};
|
|
155
414
|
}
|
|
156
415
|
});
|
|
157
416
|
|
|
158
|
-
// ../utils/src/
|
|
159
|
-
var
|
|
160
|
-
"../utils/src/
|
|
417
|
+
// ../git-utils/src/git-analyzer.types.ts
|
|
418
|
+
var init_git_analyzer_types = __esm({
|
|
419
|
+
"../git-utils/src/git-analyzer.types.ts"() {
|
|
161
420
|
"use strict";
|
|
162
421
|
init_esm_shims();
|
|
163
|
-
init_security();
|
|
164
|
-
init_string();
|
|
165
|
-
init_ui();
|
|
166
422
|
}
|
|
167
423
|
});
|
|
168
424
|
|
|
169
|
-
// ../
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
425
|
+
// ../git-utils/src/git-service.ts
|
|
426
|
+
import { execSync as execSync2 } from "child_process";
|
|
427
|
+
var GitService;
|
|
428
|
+
var init_git_service = __esm({
|
|
429
|
+
"../git-utils/src/git-service.ts"() {
|
|
430
|
+
"use strict";
|
|
431
|
+
init_esm_shims();
|
|
432
|
+
init_commit_message();
|
|
433
|
+
init_git();
|
|
434
|
+
GitService = class {
|
|
435
|
+
constructor(cwd = process.cwd(), options = {}) {
|
|
436
|
+
this.cwd = cwd;
|
|
437
|
+
this.ciSkipTag = options.ciSkipTag ?? DEFAULT_CI_SKIP_TAG;
|
|
438
|
+
}
|
|
439
|
+
ciSkipTag;
|
|
440
|
+
async addFiles(pattern) {
|
|
441
|
+
try {
|
|
442
|
+
execSync2(`git add ${pattern}`, {
|
|
443
|
+
cwd: this.cwd,
|
|
444
|
+
stdio: "ignore"
|
|
445
|
+
});
|
|
446
|
+
return true;
|
|
447
|
+
} catch {
|
|
448
|
+
return false;
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
async commit(message) {
|
|
452
|
+
try {
|
|
453
|
+
execSync2(`git commit -m "${message}"`, {
|
|
454
|
+
cwd: this.cwd,
|
|
455
|
+
stdio: "ignore"
|
|
456
|
+
});
|
|
457
|
+
return true;
|
|
458
|
+
} catch {
|
|
459
|
+
return false;
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
async addAndCommit(pattern, message) {
|
|
463
|
+
const added = await this.addFiles(pattern);
|
|
464
|
+
if (!added) return false;
|
|
465
|
+
return this.commit(message);
|
|
466
|
+
}
|
|
467
|
+
async commitTaskStatusChange(taskId, status) {
|
|
468
|
+
const pattern = `TASKS/task-${taskId}-*.md`;
|
|
469
|
+
const message = buildTaskStatusCommitMessage({ taskId, status, ciSkipTag: this.ciSkipTag });
|
|
470
|
+
return this.addAndCommit(pattern, message);
|
|
471
|
+
}
|
|
472
|
+
async commitTaskStatusChangeOnBranch(taskId, status, defaultBranch) {
|
|
473
|
+
if (!defaultBranch) {
|
|
474
|
+
return this.commitTaskStatusChange(taskId, status);
|
|
475
|
+
}
|
|
476
|
+
try {
|
|
477
|
+
const currentBranch = execSync2("git rev-parse --abbrev-ref HEAD", {
|
|
478
|
+
cwd: this.cwd,
|
|
479
|
+
encoding: "utf8",
|
|
480
|
+
stdio: "pipe"
|
|
481
|
+
}).trim();
|
|
482
|
+
if (currentBranch === defaultBranch) {
|
|
483
|
+
return this.commitTaskStatusChange(taskId, status);
|
|
484
|
+
}
|
|
485
|
+
const taskPattern = `TASKS/task-${taskId}-*.md`;
|
|
486
|
+
let taskFileContent = null;
|
|
487
|
+
let taskFilePath = null;
|
|
488
|
+
try {
|
|
489
|
+
const files = execSync2(`git ls-files -m ${taskPattern}`, {
|
|
490
|
+
cwd: this.cwd,
|
|
491
|
+
encoding: "utf8",
|
|
492
|
+
stdio: "pipe"
|
|
493
|
+
}).trim();
|
|
494
|
+
if (files) {
|
|
495
|
+
taskFilePath = files.split("\n")[0] ?? null;
|
|
496
|
+
const { readFileSync: readFileSync4 } = await import("fs");
|
|
497
|
+
taskFileContent = readFileSync4(`${this.cwd}/${taskFilePath}`, "utf-8");
|
|
498
|
+
}
|
|
499
|
+
} catch {
|
|
500
|
+
}
|
|
501
|
+
const hasChanges = await this.hasUncommittedChanges();
|
|
502
|
+
let stashed = false;
|
|
503
|
+
try {
|
|
504
|
+
if (hasChanges) {
|
|
505
|
+
execSync2('git stash push -u -m "taskin-temp-stash"', {
|
|
506
|
+
cwd: this.cwd,
|
|
507
|
+
stdio: "ignore"
|
|
508
|
+
});
|
|
509
|
+
stashed = true;
|
|
510
|
+
}
|
|
511
|
+
execSync2(`git checkout ${defaultBranch}`, {
|
|
512
|
+
cwd: this.cwd,
|
|
513
|
+
stdio: "ignore"
|
|
514
|
+
});
|
|
515
|
+
let committed = false;
|
|
516
|
+
if (taskFileContent && taskFilePath) {
|
|
517
|
+
const { writeFileSync: writeFileSync3 } = await import("fs");
|
|
518
|
+
writeFileSync3(`${this.cwd}/${taskFilePath}`, taskFileContent, "utf-8");
|
|
519
|
+
const message = buildTaskStatusCommitMessage({ taskId, status, ciSkipTag: this.ciSkipTag });
|
|
520
|
+
committed = await this.addAndCommit(taskPattern, message);
|
|
521
|
+
} else {
|
|
522
|
+
const message = buildTaskStatusCommitMessage({ taskId, status, ciSkipTag: this.ciSkipTag });
|
|
523
|
+
committed = await this.addAndCommit(taskPattern, message);
|
|
524
|
+
}
|
|
525
|
+
execSync2(`git checkout ${currentBranch}`, {
|
|
526
|
+
cwd: this.cwd,
|
|
527
|
+
stdio: "ignore"
|
|
528
|
+
});
|
|
529
|
+
if (stashed) {
|
|
530
|
+
execSync2("git stash pop", {
|
|
531
|
+
cwd: this.cwd,
|
|
532
|
+
stdio: "ignore"
|
|
533
|
+
});
|
|
534
|
+
}
|
|
535
|
+
return committed;
|
|
536
|
+
} catch {
|
|
537
|
+
try {
|
|
538
|
+
execSync2(`git checkout ${currentBranch}`, {
|
|
539
|
+
cwd: this.cwd,
|
|
540
|
+
stdio: "ignore"
|
|
541
|
+
});
|
|
542
|
+
if (stashed) {
|
|
543
|
+
execSync2("git stash pop", {
|
|
544
|
+
cwd: this.cwd,
|
|
545
|
+
stdio: "ignore"
|
|
546
|
+
});
|
|
547
|
+
}
|
|
548
|
+
} catch {
|
|
549
|
+
}
|
|
550
|
+
return false;
|
|
551
|
+
}
|
|
552
|
+
} catch {
|
|
553
|
+
return false;
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
async hasUncommittedChanges() {
|
|
557
|
+
try {
|
|
558
|
+
const output = execSync2("git status --porcelain", {
|
|
559
|
+
cwd: this.cwd,
|
|
560
|
+
encoding: "utf8",
|
|
561
|
+
stdio: "pipe"
|
|
562
|
+
});
|
|
563
|
+
return output.trim().length > 0;
|
|
564
|
+
} catch {
|
|
565
|
+
return false;
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
async getCurrentBranch() {
|
|
569
|
+
try {
|
|
570
|
+
return execSync2("git branch --show-current", {
|
|
571
|
+
cwd: this.cwd,
|
|
572
|
+
encoding: "utf8",
|
|
573
|
+
stdio: "pipe"
|
|
574
|
+
}).trim();
|
|
575
|
+
} catch {
|
|
576
|
+
return "";
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
async isGitRepository() {
|
|
580
|
+
return Promise.resolve(isGitRepository());
|
|
581
|
+
}
|
|
582
|
+
async createBranch(branchName, baseBranch) {
|
|
583
|
+
try {
|
|
584
|
+
createBranch(branchName, baseBranch);
|
|
585
|
+
return true;
|
|
586
|
+
} catch {
|
|
587
|
+
return false;
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
async checkoutBranch(branchName) {
|
|
591
|
+
try {
|
|
592
|
+
execSync2(`git checkout ${branchName}`, {
|
|
593
|
+
cwd: this.cwd,
|
|
594
|
+
stdio: "ignore"
|
|
595
|
+
});
|
|
596
|
+
return true;
|
|
597
|
+
} catch {
|
|
598
|
+
return false;
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
async fetch(remote = "origin") {
|
|
602
|
+
try {
|
|
603
|
+
execSync2(`git fetch ${remote}`, {
|
|
604
|
+
cwd: this.cwd,
|
|
605
|
+
stdio: "ignore"
|
|
606
|
+
});
|
|
607
|
+
return true;
|
|
608
|
+
} catch {
|
|
609
|
+
return false;
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
async rebase(branch) {
|
|
613
|
+
try {
|
|
614
|
+
execSync2(`git rebase ${branch}`, {
|
|
615
|
+
cwd: this.cwd,
|
|
616
|
+
stdio: "ignore"
|
|
617
|
+
});
|
|
618
|
+
return true;
|
|
619
|
+
} catch {
|
|
620
|
+
return false;
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
async push(branch, remote = "origin") {
|
|
624
|
+
try {
|
|
625
|
+
execSync2(`git push ${remote} ${branch}`, {
|
|
626
|
+
cwd: this.cwd,
|
|
627
|
+
stdio: "ignore"
|
|
628
|
+
});
|
|
629
|
+
return true;
|
|
630
|
+
} catch {
|
|
631
|
+
return false;
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
async abortRebase() {
|
|
635
|
+
try {
|
|
636
|
+
execSync2("git rebase --abort", {
|
|
637
|
+
cwd: this.cwd,
|
|
638
|
+
stdio: "ignore"
|
|
639
|
+
});
|
|
640
|
+
return true;
|
|
641
|
+
} catch {
|
|
642
|
+
return false;
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
async checkoutFile(branch, pattern) {
|
|
646
|
+
try {
|
|
647
|
+
execSync2(`git checkout ${branch} -- ${pattern}`, {
|
|
648
|
+
cwd: this.cwd,
|
|
649
|
+
stdio: "ignore"
|
|
650
|
+
});
|
|
651
|
+
return true;
|
|
652
|
+
} catch {
|
|
653
|
+
return false;
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
};
|
|
230
657
|
}
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
}
|
|
237
|
-
function validateSeededUsers(assignees, registry) {
|
|
238
|
-
const referenced = new Set(
|
|
239
|
-
assignees.flatMap((raw) => {
|
|
240
|
-
if (raw === void 0) return [];
|
|
241
|
-
const identity = classifyAssignee(raw, registry);
|
|
242
|
-
return identity.kind === "resolved" || identity.kind === "correctable" ? [identity.user.id] : [];
|
|
243
|
-
})
|
|
244
|
-
);
|
|
245
|
-
return registry.getAllUsers().filter((user) => isSeededUser(user) && !referenced.has(user.id)).map((user) => ({
|
|
246
|
-
file: `${".taskin/.taskin-users.json"} (${user.id})`,
|
|
247
|
-
message: `User "${user.id}" looks like the placeholder that older taskin versions seeded on init (${user.email}), and no task points at it.`,
|
|
248
|
-
severity: "warning",
|
|
249
|
-
suggestion: `Remove it from the registry, or give it the real name and e-mail of whoever it stands for.`
|
|
250
|
-
}));
|
|
251
|
-
}
|
|
252
|
-
var PLACEHOLDER_ASSIGNEES, foldAssignee, fold;
|
|
253
|
-
var init_assignee_identity = __esm({
|
|
254
|
-
"../file-system-task-provider/src/assignee-identity.ts"() {
|
|
658
|
+
});
|
|
659
|
+
|
|
660
|
+
// ../git-utils/src/git-service.types.ts
|
|
661
|
+
var init_git_service_types = __esm({
|
|
662
|
+
"../git-utils/src/git-service.types.ts"() {
|
|
255
663
|
"use strict";
|
|
256
664
|
init_esm_shims();
|
|
257
|
-
PLACEHOLDER_ASSIGNEES = [
|
|
258
|
-
"a definir",
|
|
259
|
-
"to be defined",
|
|
260
|
-
"nome do responsavel",
|
|
261
|
-
"nome do respons\xE1vel",
|
|
262
|
-
"tbd",
|
|
263
|
-
"-"
|
|
264
|
-
];
|
|
265
|
-
foldAssignee = (value) => value.toLowerCase().replace(/[^a-z0-9]/g, "");
|
|
266
|
-
fold = foldAssignee;
|
|
267
665
|
}
|
|
268
666
|
});
|
|
269
667
|
|
|
270
|
-
// ../
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
668
|
+
// ../git-utils/src/index.ts
|
|
669
|
+
var init_src = __esm({
|
|
670
|
+
"../git-utils/src/index.ts"() {
|
|
671
|
+
"use strict";
|
|
672
|
+
init_esm_shims();
|
|
673
|
+
init_commit_message();
|
|
674
|
+
init_git();
|
|
675
|
+
init_git_types();
|
|
676
|
+
init_git_analyzer();
|
|
677
|
+
init_git_analyzer_types();
|
|
678
|
+
init_git_service();
|
|
679
|
+
init_git_service_types();
|
|
274
680
|
}
|
|
275
|
-
|
|
681
|
+
});
|
|
682
|
+
|
|
683
|
+
// ../utils/src/security.ts
|
|
684
|
+
import { z } from "zod";
|
|
685
|
+
function isValidHost(host) {
|
|
686
|
+
return HostSchema.safeParse(host).success;
|
|
276
687
|
}
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
if (config.autoSync && !config.defaultBranch) {
|
|
280
|
-
console.warn("autoSync is enabled but no defaultBranch is configured. Nothing will be synced.");
|
|
281
|
-
}
|
|
282
|
-
return;
|
|
283
|
-
}
|
|
284
|
-
const fetchOk = await safeCall(() => git.fetch());
|
|
285
|
-
if (!fetchOk) {
|
|
286
|
-
throw new Error("Fetch failed. Check your network connection.");
|
|
287
|
-
}
|
|
288
|
-
const rebaseOk = await safeCall(() => git.rebase(`origin/${config.defaultBranch}`));
|
|
289
|
-
if (!rebaseOk) {
|
|
290
|
-
await git.abortRebase();
|
|
291
|
-
throw new Error("Rebase failed due to conflict. Aborted.");
|
|
292
|
-
}
|
|
688
|
+
function isValidPort(port) {
|
|
689
|
+
return PortSchema.safeParse(port).success;
|
|
293
690
|
}
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
return
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
|
|
691
|
+
function escapeHtml(text) {
|
|
692
|
+
if (!text || typeof text !== "string") {
|
|
693
|
+
return "";
|
|
694
|
+
}
|
|
695
|
+
const htmlEscapeMap = {
|
|
696
|
+
"&": "&",
|
|
697
|
+
"<": "<",
|
|
698
|
+
">": ">",
|
|
699
|
+
'"': """,
|
|
700
|
+
"'": "'",
|
|
701
|
+
"/": "/"
|
|
702
|
+
};
|
|
703
|
+
return text.replace(/[&<>"'/]/g, (char) => htmlEscapeMap[char]);
|
|
300
704
|
}
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
705
|
+
var HostSchema, PortSchema, WebSocketUrlSchema, TaskIdSchema2, UserIdSchema, EmailSchema, SafePathSchema, DashboardOptionsSchema;
|
|
706
|
+
var init_security = __esm({
|
|
707
|
+
"../utils/src/security.ts"() {
|
|
708
|
+
"use strict";
|
|
709
|
+
init_esm_shims();
|
|
710
|
+
HostSchema = z.string().refine(
|
|
711
|
+
(host) => {
|
|
712
|
+
if (!host || host.length === 0) return false;
|
|
713
|
+
if (host === "localhost") return true;
|
|
714
|
+
const parts = host.split(".");
|
|
715
|
+
const allNumeric = parts.every((p) => /^\d+$/.test(p));
|
|
716
|
+
if (allNumeric) {
|
|
717
|
+
if (parts.length !== 4) return false;
|
|
718
|
+
return parts.every((part) => {
|
|
719
|
+
const num = parseInt(part, 10);
|
|
720
|
+
return !Number.isNaN(num) && num >= 0 && num <= 255 && part === num.toString();
|
|
721
|
+
});
|
|
722
|
+
}
|
|
723
|
+
const hostnameRegex = /^[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(\.[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/;
|
|
724
|
+
return hostnameRegex.test(host);
|
|
725
|
+
},
|
|
726
|
+
{
|
|
727
|
+
message: "Invalid host. Must be localhost, a valid IPv4 address, or hostname."
|
|
322
728
|
}
|
|
323
|
-
|
|
324
|
-
|
|
729
|
+
);
|
|
730
|
+
PortSchema = z.union([
|
|
731
|
+
z.number().int().min(1).max(65535),
|
|
732
|
+
z.string().regex(/^\d+$/).transform((val) => parseInt(val, 10)).pipe(z.number().int().min(1).max(65535))
|
|
733
|
+
]);
|
|
734
|
+
WebSocketUrlSchema = z.string().refine(
|
|
735
|
+
(url) => {
|
|
736
|
+
try {
|
|
737
|
+
const parsed = new URL(url);
|
|
738
|
+
return parsed.protocol === "ws:" || parsed.protocol === "wss:";
|
|
739
|
+
} catch {
|
|
740
|
+
return false;
|
|
741
|
+
}
|
|
742
|
+
},
|
|
743
|
+
{ message: "Invalid WebSocket URL. Must use ws:// or wss:// protocol." }
|
|
744
|
+
);
|
|
745
|
+
TaskIdSchema2 = z.string().min(1).max(100).regex(/^[a-zA-Z0-9_-]+$/, {
|
|
746
|
+
message: "Task ID must contain only alphanumeric characters, hyphens, and underscores."
|
|
747
|
+
});
|
|
748
|
+
UserIdSchema = z.string().min(1).max(100).regex(/^[a-zA-Z0-9._-]+$/, {
|
|
749
|
+
message: "User ID must contain only alphanumeric characters, dots, hyphens, and underscores."
|
|
750
|
+
});
|
|
751
|
+
EmailSchema = z.string().email().max(254);
|
|
752
|
+
SafePathSchema = z.string().refine(
|
|
753
|
+
(filePath) => {
|
|
754
|
+
if (!filePath || filePath.length === 0) return false;
|
|
755
|
+
const dangerousPatterns = [
|
|
756
|
+
/\.\./,
|
|
757
|
+
// Parent directory (..)
|
|
758
|
+
/~\//,
|
|
759
|
+
// Home directory
|
|
760
|
+
/^\//,
|
|
761
|
+
// Absolute path
|
|
762
|
+
/^[A-Za-z]:\\/
|
|
763
|
+
// Windows absolute path
|
|
764
|
+
];
|
|
765
|
+
return !dangerousPatterns.some((pattern) => pattern.test(filePath));
|
|
766
|
+
},
|
|
767
|
+
{
|
|
768
|
+
message: "Invalid path. Must be a relative path without traversal patterns."
|
|
325
769
|
}
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
}
|
|
332
|
-
throw new Error(`Push rejected after ${maxAttempts} retries. Exhausted retry limit.`);
|
|
333
|
-
}
|
|
334
|
-
async function pushAfterCreate(git, options) {
|
|
335
|
-
const pattern = `TASKS/task-${options.taskId}-*.md`;
|
|
336
|
-
const message = `docs(TASKS): task-${options.taskId} - ${options.title} [skip-ci]`;
|
|
337
|
-
await attemptPushWithRetry(git, pattern, message, options.defaultBranch, MAX_RETRY_ATTEMPTS);
|
|
338
|
-
return true;
|
|
339
|
-
}
|
|
340
|
-
async function getNextTaskNumberAfterSync(options) {
|
|
341
|
-
const maxLocal = options.localCount;
|
|
342
|
-
const maxRemote = options.remoteCount;
|
|
343
|
-
if (options.autoSync) {
|
|
344
|
-
const maxNumber = Math.max(maxLocal, maxRemote);
|
|
345
|
-
return maxNumber + 1;
|
|
346
|
-
}
|
|
347
|
-
return maxLocal + 1;
|
|
348
|
-
}
|
|
349
|
-
async function createTaskWithSync(git, config, taskOptions) {
|
|
350
|
-
await syncBeforeCreate(git, config);
|
|
351
|
-
const nextNumber = await getNextTaskNumberAfterSync({
|
|
352
|
-
autoSync: config.autoSync,
|
|
353
|
-
localCount: 0,
|
|
354
|
-
remoteCount: 0
|
|
355
|
-
});
|
|
356
|
-
const taskId = String(nextNumber).padStart(3, "0");
|
|
357
|
-
if (config.autoSync && config.defaultBranch) {
|
|
358
|
-
await pushAfterCreate(git, {
|
|
359
|
-
taskId,
|
|
360
|
-
title: taskOptions.title,
|
|
361
|
-
defaultBranch: config.defaultBranch
|
|
770
|
+
);
|
|
771
|
+
DashboardOptionsSchema = z.object({
|
|
772
|
+
host: HostSchema.optional(),
|
|
773
|
+
port: PortSchema.optional(),
|
|
774
|
+
wsPort: PortSchema.optional()
|
|
362
775
|
});
|
|
363
776
|
}
|
|
364
|
-
|
|
777
|
+
});
|
|
778
|
+
|
|
779
|
+
// ../utils/src/string.ts
|
|
780
|
+
function slugify(text) {
|
|
781
|
+
return text.normalize("NFD").replace(/[\u0300-\u036f]/g, "").toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
|
|
365
782
|
}
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
const currentBranch = await git.getCurrentBranch();
|
|
371
|
-
const originBranch = options.originBranch;
|
|
372
|
-
const pattern = `TASKS/task-${options.taskId}-*.md`;
|
|
373
|
-
const assetsPattern = `TASKS/assets/task-${options.taskId}/`;
|
|
374
|
-
const coResult = await git.checkoutBranch(originBranch);
|
|
375
|
-
if (!coResult) {
|
|
376
|
-
await git.checkoutBranch(currentBranch);
|
|
377
|
-
return false;
|
|
378
|
-
}
|
|
379
|
-
try {
|
|
380
|
-
const patternsToAdd = [];
|
|
381
|
-
const fileOk = await git.checkoutFile(options.defaultBranch, pattern);
|
|
382
|
-
if (fileOk) {
|
|
383
|
-
patternsToAdd.push(pattern);
|
|
384
|
-
}
|
|
385
|
-
const assetsOk = await git.checkoutFile(options.defaultBranch, assetsPattern);
|
|
386
|
-
if (assetsOk) {
|
|
387
|
-
patternsToAdd.push(assetsPattern);
|
|
388
|
-
}
|
|
389
|
-
if (patternsToAdd.length === 0) {
|
|
390
|
-
await git.checkoutBranch(currentBranch);
|
|
391
|
-
return false;
|
|
392
|
-
}
|
|
393
|
-
const combinedPattern = patternsToAdd.join(" ");
|
|
394
|
-
const addOk = await git.addFiles(combinedPattern);
|
|
395
|
-
if (!addOk) {
|
|
396
|
-
await git.checkoutBranch(currentBranch);
|
|
397
|
-
return false;
|
|
398
|
-
}
|
|
399
|
-
const message = `docs(TASKS): task-${options.taskId} - done [skip-ci]`;
|
|
400
|
-
const commitOk = await git.commit(message);
|
|
401
|
-
if (!commitOk) {
|
|
402
|
-
await git.checkoutBranch(currentBranch);
|
|
403
|
-
return false;
|
|
404
|
-
}
|
|
405
|
-
for (let attempt = 1; attempt <= MAX_RETRY_ATTEMPTS; attempt++) {
|
|
406
|
-
if (attempt > 1) {
|
|
407
|
-
await safeCall(() => git.fetch());
|
|
408
|
-
await safeCall(() => git.rebase(`origin/${originBranch}`));
|
|
409
|
-
}
|
|
410
|
-
const pushOk = await safeCall(() => git.push(originBranch));
|
|
411
|
-
if (pushOk === true) {
|
|
412
|
-
await git.checkoutBranch(currentBranch);
|
|
413
|
-
return true;
|
|
414
|
-
}
|
|
415
|
-
if (attempt >= MAX_RETRY_ATTEMPTS) {
|
|
416
|
-
await git.checkoutBranch(currentBranch);
|
|
417
|
-
throw new Error(`Squash push rejected after ${MAX_RETRY_ATTEMPTS} retries.`);
|
|
418
|
-
}
|
|
419
|
-
}
|
|
420
|
-
await git.checkoutBranch(currentBranch);
|
|
421
|
-
throw new Error(`Squash push rejected after ${MAX_RETRY_ATTEMPTS} retries.`);
|
|
422
|
-
} catch (error2) {
|
|
423
|
-
await git.checkoutBranch(currentBranch);
|
|
424
|
-
if (error2 instanceof Error) {
|
|
425
|
-
throw error2;
|
|
426
|
-
}
|
|
427
|
-
return false;
|
|
783
|
+
var init_string = __esm({
|
|
784
|
+
"../utils/src/string.ts"() {
|
|
785
|
+
"use strict";
|
|
786
|
+
init_esm_shims();
|
|
428
787
|
}
|
|
429
|
-
}
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
788
|
+
});
|
|
789
|
+
|
|
790
|
+
// ../utils/src/ui.ts
|
|
791
|
+
import chalk3 from "chalk";
|
|
792
|
+
var colors2;
|
|
793
|
+
var init_ui = __esm({
|
|
794
|
+
"../utils/src/ui.ts"() {
|
|
433
795
|
"use strict";
|
|
434
796
|
init_esm_shims();
|
|
435
|
-
|
|
797
|
+
colors2 = {
|
|
798
|
+
primary: chalk3.blue,
|
|
799
|
+
secondary: chalk3.gray,
|
|
800
|
+
success: chalk3.green,
|
|
801
|
+
warning: chalk3.yellow,
|
|
802
|
+
error: chalk3.red,
|
|
803
|
+
info: chalk3.cyan,
|
|
804
|
+
highlight: chalk3.magenta,
|
|
805
|
+
normal: chalk3.white
|
|
806
|
+
};
|
|
436
807
|
}
|
|
437
808
|
});
|
|
438
809
|
|
|
439
|
-
// ../
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
}
|
|
443
|
-
var HARD_BREAK;
|
|
444
|
-
var init_inline_metadata = __esm({
|
|
445
|
-
"../file-system-task-provider/src/inline-metadata.ts"() {
|
|
810
|
+
// ../utils/src/index.ts
|
|
811
|
+
var init_src2 = __esm({
|
|
812
|
+
"../utils/src/index.ts"() {
|
|
446
813
|
"use strict";
|
|
447
814
|
init_esm_shims();
|
|
448
|
-
|
|
815
|
+
init_security();
|
|
816
|
+
init_string();
|
|
817
|
+
init_ui();
|
|
449
818
|
}
|
|
450
819
|
});
|
|
451
820
|
|
|
452
|
-
// ../file-system-task-provider/src/
|
|
453
|
-
function
|
|
454
|
-
|
|
455
|
-
return index === -1 ? lines.length : index;
|
|
821
|
+
// ../file-system-task-provider/src/i18n.ts
|
|
822
|
+
function getI18n(locale = "en-US") {
|
|
823
|
+
return i18nConfig[locale];
|
|
456
824
|
}
|
|
457
|
-
function
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
825
|
+
function detectLocale(content) {
|
|
826
|
+
if (content.includes("## Descri\xE7\xE3o") || content.includes("## Tipo") || content.includes("## Respons\xE1vel") || content.includes("## Tarefas")) {
|
|
827
|
+
return "pt-BR";
|
|
828
|
+
}
|
|
829
|
+
return "en-US";
|
|
830
|
+
}
|
|
831
|
+
var i18nConfig;
|
|
832
|
+
var init_i18n = __esm({
|
|
833
|
+
"../file-system-task-provider/src/i18n.ts"() {
|
|
834
|
+
"use strict";
|
|
835
|
+
init_esm_shims();
|
|
836
|
+
i18nConfig = {
|
|
837
|
+
"en-US": {
|
|
838
|
+
status: "Status",
|
|
839
|
+
type: "Type",
|
|
840
|
+
assignee: "Assignee",
|
|
841
|
+
description: "Description",
|
|
842
|
+
tasks: "Tasks",
|
|
843
|
+
notes: "Notes",
|
|
844
|
+
defaultAssignee: "To be defined",
|
|
845
|
+
descriptionPlaceholder: "Add task description here...",
|
|
846
|
+
notesPlaceholder: "Add any relevant notes or links here.",
|
|
847
|
+
priority: "Priority",
|
|
848
|
+
group: "Group",
|
|
849
|
+
groupName: "GroupName",
|
|
850
|
+
difficulty: "Difficulty"
|
|
851
|
+
},
|
|
852
|
+
"pt-BR": {
|
|
853
|
+
status: "Status",
|
|
854
|
+
type: "Tipo",
|
|
855
|
+
assignee: "Respons\xE1vel",
|
|
856
|
+
description: "Descri\xE7\xE3o",
|
|
857
|
+
tasks: "Tarefas",
|
|
858
|
+
notes: "Notas",
|
|
859
|
+
defaultAssignee: "A definir",
|
|
860
|
+
descriptionPlaceholder: "Adicione a descri\xE7\xE3o da tarefa aqui...",
|
|
861
|
+
notesPlaceholder: "Adicione notas ou links relevantes aqui.",
|
|
862
|
+
priority: "Prioridade",
|
|
863
|
+
group: "Grupo",
|
|
864
|
+
groupName: "NomeGrupo",
|
|
865
|
+
difficulty: "Dificuldade"
|
|
866
|
+
}
|
|
867
|
+
};
|
|
868
|
+
}
|
|
869
|
+
});
|
|
870
|
+
|
|
871
|
+
// ../file-system-task-provider/src/inline-metadata.ts
|
|
872
|
+
function stripHardBreak(value) {
|
|
873
|
+
return value.replace(/\\$/, "").trim();
|
|
874
|
+
}
|
|
875
|
+
var HARD_BREAK;
|
|
876
|
+
var init_inline_metadata = __esm({
|
|
877
|
+
"../file-system-task-provider/src/inline-metadata.ts"() {
|
|
878
|
+
"use strict";
|
|
879
|
+
init_esm_shims();
|
|
880
|
+
HARD_BREAK = "\\";
|
|
881
|
+
}
|
|
882
|
+
});
|
|
883
|
+
|
|
884
|
+
// ../file-system-task-provider/src/metadata-style/metadata-block.ts
|
|
885
|
+
function headerEndIndex(lines) {
|
|
886
|
+
const index = lines.findIndex((line) => SECTION_HEADING.test(line));
|
|
887
|
+
return index === -1 ? lines.length : index;
|
|
888
|
+
}
|
|
889
|
+
function readMetadataBlock(content) {
|
|
890
|
+
const lines = splitLines(content);
|
|
891
|
+
const headerEnd = headerEndIndex(lines);
|
|
892
|
+
let start = -1;
|
|
461
893
|
for (let index = 0; index < headerEnd; index++) {
|
|
462
894
|
const line = lines[index] ?? "";
|
|
463
895
|
if (isHeading(line) || line.trim() === "") continue;
|
|
@@ -468,16 +900,24 @@ function readMetadataBlock(content) {
|
|
|
468
900
|
}
|
|
469
901
|
if (start === -1) return void 0;
|
|
470
902
|
let end = start;
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
903
|
+
let cursor = start;
|
|
904
|
+
while (cursor < headerEnd) {
|
|
905
|
+
const line = lines[cursor] ?? "";
|
|
906
|
+
if (isHeading(line)) break;
|
|
907
|
+
if (line.trim() === "") {
|
|
908
|
+
cursor++;
|
|
909
|
+
continue;
|
|
910
|
+
}
|
|
911
|
+
if (!METADATA_LINE.test(line)) break;
|
|
912
|
+
cursor++;
|
|
913
|
+
end = cursor;
|
|
475
914
|
}
|
|
476
|
-
const blockLines = lines.slice(start, end);
|
|
477
915
|
const fields = [];
|
|
478
|
-
|
|
916
|
+
const blockLines = [];
|
|
917
|
+
for (const line of lines.slice(start, end)) {
|
|
479
918
|
const match = line.match(METADATA_LINE);
|
|
480
919
|
if (!match?.[1]) continue;
|
|
920
|
+
blockLines.push(line);
|
|
481
921
|
fields.push({ label: match[1], value: match[2] ?? "" });
|
|
482
922
|
}
|
|
483
923
|
return { fields, lines: blockLines, start, end };
|
|
@@ -509,7 +949,7 @@ var init_metadata_block = __esm({
|
|
|
509
949
|
"../file-system-task-provider/src/metadata-style/metadata-block.ts"() {
|
|
510
950
|
"use strict";
|
|
511
951
|
init_esm_shims();
|
|
512
|
-
METADATA_LINE = /^(?:-[ \t]+)?([^:\n]{
|
|
952
|
+
METADATA_LINE = /^(?:-[ \t]+)?([\p{L}\p{N}][^:\n]{0,39}?)[ \t]*:[ \t]*(.*?)[ \t]*\\?[ \t]*$/u;
|
|
513
953
|
SECTION_HEADING = /^#{2,}\s/;
|
|
514
954
|
isHeading = (line) => line.startsWith("#");
|
|
515
955
|
splitLines = (content) => content.split(/\r?\n/);
|
|
@@ -638,7 +1078,9 @@ function convertMetadataStyle(content, target) {
|
|
|
638
1078
|
const block = readMetadataBlock(content);
|
|
639
1079
|
if (!block || block.fields.length === 0) return content;
|
|
640
1080
|
const lines = getMetadataStyle(target).format(block.fields).split("\n");
|
|
641
|
-
|
|
1081
|
+
const intacto = lines.join("\n") === block.lines.join("\n");
|
|
1082
|
+
const contiguo = block.end - block.start === block.lines.length;
|
|
1083
|
+
if (intacto && contiguo) return content;
|
|
642
1084
|
return replaceMetadataBlock(content, lines);
|
|
643
1085
|
}
|
|
644
1086
|
var DEFAULT_METADATA_STYLE_ID, DETECTION_ORDER, METADATA_STYLES, METADATA_STYLE_IDS;
|
|
@@ -673,89 +1115,368 @@ var init_metadata_style2 = __esm({
|
|
|
673
1115
|
}
|
|
674
1116
|
});
|
|
675
1117
|
|
|
676
|
-
// ../file-system-task-provider/src/
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
}
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
1118
|
+
// ../file-system-task-provider/src/assignee-identity.ts
|
|
1119
|
+
function classifyAssignee(raw, registry) {
|
|
1120
|
+
const trimmed = raw.trim();
|
|
1121
|
+
if (trimmed === "" || PLACEHOLDER_ASSIGNEES.includes(trimmed.toLowerCase())) {
|
|
1122
|
+
return { kind: "unassigned", raw };
|
|
1123
|
+
}
|
|
1124
|
+
const user = registry.resolveUser(trimmed);
|
|
1125
|
+
if (user) {
|
|
1126
|
+
return { kind: "resolved", raw, user };
|
|
1127
|
+
}
|
|
1128
|
+
const folded = fold(trimmed);
|
|
1129
|
+
const matches = registry.getAllUsers().filter((candidate) => fold(candidate.id) === folded || fold(candidate.name) === folded);
|
|
1130
|
+
const [onlyMatch] = matches;
|
|
1131
|
+
if (onlyMatch && matches.length === 1) {
|
|
1132
|
+
return { kind: "correctable", raw, user: onlyMatch };
|
|
1133
|
+
}
|
|
1134
|
+
return { kind: "unknown", raw };
|
|
686
1135
|
}
|
|
687
|
-
function
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
1136
|
+
function validateAssignees(tasks, registry) {
|
|
1137
|
+
const issues = [];
|
|
1138
|
+
for (const task of tasks) {
|
|
1139
|
+
if (task.assignee === void 0) continue;
|
|
1140
|
+
const identity = classifyAssignee(task.assignee, registry);
|
|
1141
|
+
switch (identity.kind) {
|
|
1142
|
+
case "resolved":
|
|
1143
|
+
case "unassigned":
|
|
1144
|
+
break;
|
|
1145
|
+
case "correctable":
|
|
1146
|
+
issues.push({
|
|
1147
|
+
file: task.file,
|
|
1148
|
+
message: `Assignee "${identity.raw.trim()}" is not in the user registry, but folds onto exactly one registered user.`,
|
|
1149
|
+
severity: "warning",
|
|
1150
|
+
suggestion: `Rewrite it as "${identity.user.id}" \u2014 lint --fix does this.`
|
|
1151
|
+
});
|
|
1152
|
+
break;
|
|
1153
|
+
case "unknown":
|
|
1154
|
+
issues.push({
|
|
1155
|
+
file: task.file,
|
|
1156
|
+
message: `Assignee "${identity.raw.trim()}" resolves to nobody in the user registry, so it silently becomes a fabricated temporary user.`,
|
|
1157
|
+
severity: "warning",
|
|
1158
|
+
suggestion: `Register them in ${".taskin/.taskin-users.json"}, or fix the spelling \u2014 too ambiguous for --fix to decide.`
|
|
1159
|
+
});
|
|
1160
|
+
break;
|
|
1161
|
+
default:
|
|
1162
|
+
identity;
|
|
1163
|
+
}
|
|
1164
|
+
}
|
|
1165
|
+
return issues;
|
|
696
1166
|
}
|
|
697
|
-
function
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
}
|
|
1167
|
+
async function fixAssignees(tasks, registry, io) {
|
|
1168
|
+
const rewritten = [];
|
|
1169
|
+
for (const task of tasks) {
|
|
1170
|
+
if (task.assignee === void 0) continue;
|
|
1171
|
+
const identity = classifyAssignee(task.assignee, registry);
|
|
1172
|
+
if (identity.kind !== "correctable") continue;
|
|
1173
|
+
const content = await io.readFile(task.file);
|
|
1174
|
+
const i18n = getI18n(detectLocale(content));
|
|
1175
|
+
const label = readMetadataField(content, i18n.assignee) === void 0 ? "Assignee" : i18n.assignee;
|
|
1176
|
+
const next = writeMetadataField(content, label, identity.user.id);
|
|
1177
|
+
if (next !== content) {
|
|
1178
|
+
await io.writeFile(task.file, next);
|
|
1179
|
+
rewritten.push(task.file);
|
|
1180
|
+
}
|
|
1181
|
+
}
|
|
1182
|
+
return rewritten;
|
|
712
1183
|
}
|
|
713
|
-
function
|
|
714
|
-
|
|
1184
|
+
function isSeededUser(user) {
|
|
1185
|
+
const capitalized = user.id.charAt(0).toUpperCase() + user.id.slice(1);
|
|
1186
|
+
return user.email === `${user.id}@example.com` && user.name === capitalized;
|
|
715
1187
|
}
|
|
716
|
-
function
|
|
717
|
-
const
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
case "quarter":
|
|
731
|
-
since = new Date(Date.now() - 90 * MILLISECONDS_PER_DAY);
|
|
732
|
-
break;
|
|
733
|
-
case "year":
|
|
734
|
-
since = new Date(Date.now() - 365 * MILLISECONDS_PER_DAY);
|
|
735
|
-
break;
|
|
736
|
-
case "all":
|
|
737
|
-
since = /* @__PURE__ */ new Date(0);
|
|
738
|
-
break;
|
|
739
|
-
default:
|
|
740
|
-
since = new Date(Date.now() - DAYS_PER_WEEK * MILLISECONDS_PER_DAY);
|
|
741
|
-
}
|
|
742
|
-
return { since, until };
|
|
1188
|
+
function validateSeededUsers(assignees, registry) {
|
|
1189
|
+
const referenced = new Set(
|
|
1190
|
+
assignees.flatMap((raw) => {
|
|
1191
|
+
if (raw === void 0) return [];
|
|
1192
|
+
const identity = classifyAssignee(raw, registry);
|
|
1193
|
+
return identity.kind === "resolved" || identity.kind === "correctable" ? [identity.user.id] : [];
|
|
1194
|
+
})
|
|
1195
|
+
);
|
|
1196
|
+
return registry.getAllUsers().filter((user) => isSeededUser(user) && !referenced.has(user.id)).map((user) => ({
|
|
1197
|
+
file: `${".taskin/.taskin-users.json"} (${user.id})`,
|
|
1198
|
+
message: `User "${user.id}" looks like the placeholder that older taskin versions seeded on init (${user.email}), and no task points at it.`,
|
|
1199
|
+
severity: "warning",
|
|
1200
|
+
suggestion: `Remove it from the registry, or give it the real name and e-mail of whoever it stands for.`
|
|
1201
|
+
}));
|
|
743
1202
|
}
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
1203
|
+
var PLACEHOLDER_ASSIGNEES, foldAssignee, fold;
|
|
1204
|
+
var init_assignee_identity = __esm({
|
|
1205
|
+
"../file-system-task-provider/src/assignee-identity.ts"() {
|
|
1206
|
+
"use strict";
|
|
1207
|
+
init_esm_shims();
|
|
1208
|
+
init_i18n();
|
|
1209
|
+
init_metadata_style2();
|
|
1210
|
+
PLACEHOLDER_ASSIGNEES = [
|
|
1211
|
+
"a definir",
|
|
1212
|
+
"to be defined",
|
|
1213
|
+
"nome do responsavel",
|
|
1214
|
+
"nome do respons\xE1vel",
|
|
1215
|
+
"nao atribuido",
|
|
1216
|
+
"n\xE3o atribu\xEDdo",
|
|
1217
|
+
"unassigned",
|
|
1218
|
+
"tbd",
|
|
1219
|
+
"-"
|
|
1220
|
+
];
|
|
1221
|
+
foldAssignee = (value) => value.toLowerCase().replace(/[^a-z0-9]/g, "");
|
|
1222
|
+
fold = foldAssignee;
|
|
1223
|
+
}
|
|
1224
|
+
});
|
|
1225
|
+
|
|
1226
|
+
// ../file-system-task-provider/src/auto-sync.ts
|
|
1227
|
+
function isNonFastForwardError(error2) {
|
|
1228
|
+
if (error2 instanceof Error) {
|
|
1229
|
+
return error2.message.toLowerCase().includes("non-fast-forward");
|
|
1230
|
+
}
|
|
1231
|
+
return false;
|
|
755
1232
|
}
|
|
756
|
-
async function
|
|
757
|
-
if (!
|
|
758
|
-
|
|
1233
|
+
async function syncBeforeCreate(git, config) {
|
|
1234
|
+
if (!config.autoSync || !config.defaultBranch) {
|
|
1235
|
+
if (config.autoSync && !config.defaultBranch) {
|
|
1236
|
+
console.warn("autoSync is enabled but no defaultBranch is configured. Nothing will be synced.");
|
|
1237
|
+
}
|
|
1238
|
+
return;
|
|
1239
|
+
}
|
|
1240
|
+
const fetchOk = await safeCall(() => git.fetch());
|
|
1241
|
+
if (!fetchOk) {
|
|
1242
|
+
throw new Error("Fetch failed. Check your network connection.");
|
|
1243
|
+
}
|
|
1244
|
+
const rebaseOk = await safeCall(() => git.rebase(`origin/${config.defaultBranch}`));
|
|
1245
|
+
if (!rebaseOk) {
|
|
1246
|
+
await git.abortRebase();
|
|
1247
|
+
throw new Error("Rebase failed due to conflict. Aborted.");
|
|
1248
|
+
}
|
|
1249
|
+
}
|
|
1250
|
+
async function safeCall(fn) {
|
|
1251
|
+
try {
|
|
1252
|
+
return await fn();
|
|
1253
|
+
} catch {
|
|
1254
|
+
return false;
|
|
1255
|
+
}
|
|
1256
|
+
}
|
|
1257
|
+
async function attemptPushWithRetry(git, pattern, message, branch, maxAttempts) {
|
|
1258
|
+
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
1259
|
+
if (attempt > 1) {
|
|
1260
|
+
const fetchOk = await safeCall(() => git.fetch());
|
|
1261
|
+
if (!fetchOk) {
|
|
1262
|
+
throw new Error("Fetch failed during retry.");
|
|
1263
|
+
}
|
|
1264
|
+
const rebaseOk = await safeCall(() => git.rebase(`origin/${branch}`));
|
|
1265
|
+
if (!rebaseOk) {
|
|
1266
|
+
throw new Error("Rebase failed during retry.");
|
|
1267
|
+
}
|
|
1268
|
+
}
|
|
1269
|
+
await git.addAndCommit(pattern, message);
|
|
1270
|
+
try {
|
|
1271
|
+
const pushOk = await git.push(branch);
|
|
1272
|
+
if (pushOk) {
|
|
1273
|
+
return;
|
|
1274
|
+
}
|
|
1275
|
+
} catch (error2) {
|
|
1276
|
+
if (!isNonFastForwardError(error2)) {
|
|
1277
|
+
throw error2;
|
|
1278
|
+
}
|
|
1279
|
+
if (attempt >= maxAttempts) {
|
|
1280
|
+
throw new Error(`Push rejected after ${maxAttempts} retries. Exhausted retry limit.`);
|
|
1281
|
+
}
|
|
1282
|
+
continue;
|
|
1283
|
+
}
|
|
1284
|
+
if (attempt >= maxAttempts) {
|
|
1285
|
+
throw new Error(`Push rejected after ${maxAttempts} retries. Exhausted retry limit.`);
|
|
1286
|
+
}
|
|
1287
|
+
}
|
|
1288
|
+
throw new Error(`Push rejected after ${maxAttempts} retries. Exhausted retry limit.`);
|
|
1289
|
+
}
|
|
1290
|
+
async function pushAfterCreate(git, options) {
|
|
1291
|
+
const pattern = `TASKS/task-${options.taskId}-*.md`;
|
|
1292
|
+
const message = appendCiSkipTag(`docs(TASKS): task-${options.taskId} - ${options.title}`, options.ciSkipTag);
|
|
1293
|
+
await attemptPushWithRetry(git, pattern, message, options.defaultBranch, MAX_RETRY_ATTEMPTS);
|
|
1294
|
+
return true;
|
|
1295
|
+
}
|
|
1296
|
+
async function getNextTaskNumberAfterSync(options) {
|
|
1297
|
+
const maxLocal = options.localCount;
|
|
1298
|
+
const maxRemote = options.remoteCount;
|
|
1299
|
+
if (options.autoSync) {
|
|
1300
|
+
const maxNumber = Math.max(maxLocal, maxRemote);
|
|
1301
|
+
return maxNumber + 1;
|
|
1302
|
+
}
|
|
1303
|
+
return maxLocal + 1;
|
|
1304
|
+
}
|
|
1305
|
+
async function createTaskWithSync(git, config, taskOptions) {
|
|
1306
|
+
await syncBeforeCreate(git, config);
|
|
1307
|
+
const nextNumber = await getNextTaskNumberAfterSync({
|
|
1308
|
+
autoSync: config.autoSync,
|
|
1309
|
+
localCount: 0,
|
|
1310
|
+
remoteCount: 0
|
|
1311
|
+
});
|
|
1312
|
+
const taskId = String(nextNumber).padStart(3, "0");
|
|
1313
|
+
if (config.autoSync && config.defaultBranch) {
|
|
1314
|
+
await pushAfterCreate(git, {
|
|
1315
|
+
taskId,
|
|
1316
|
+
title: taskOptions.title,
|
|
1317
|
+
defaultBranch: config.defaultBranch,
|
|
1318
|
+
ciSkipTag: config.ciSkipTag
|
|
1319
|
+
});
|
|
1320
|
+
}
|
|
1321
|
+
return { taskId };
|
|
1322
|
+
}
|
|
1323
|
+
async function squashTaskFileOnDone(git, options) {
|
|
1324
|
+
if (!options.originBranch) {
|
|
1325
|
+
return false;
|
|
1326
|
+
}
|
|
1327
|
+
const currentBranch = await git.getCurrentBranch();
|
|
1328
|
+
const originBranch = options.originBranch;
|
|
1329
|
+
const pattern = `TASKS/task-${options.taskId}-*.md`;
|
|
1330
|
+
const assetsPattern = `TASKS/assets/task-${options.taskId}/`;
|
|
1331
|
+
const coResult = await git.checkoutBranch(originBranch);
|
|
1332
|
+
if (!coResult) {
|
|
1333
|
+
await git.checkoutBranch(currentBranch);
|
|
1334
|
+
return false;
|
|
1335
|
+
}
|
|
1336
|
+
try {
|
|
1337
|
+
const patternsToAdd = [];
|
|
1338
|
+
const fileOk = await git.checkoutFile(options.defaultBranch, pattern);
|
|
1339
|
+
if (fileOk) {
|
|
1340
|
+
patternsToAdd.push(pattern);
|
|
1341
|
+
}
|
|
1342
|
+
const assetsOk = await git.checkoutFile(options.defaultBranch, assetsPattern);
|
|
1343
|
+
if (assetsOk) {
|
|
1344
|
+
patternsToAdd.push(assetsPattern);
|
|
1345
|
+
}
|
|
1346
|
+
if (patternsToAdd.length === 0) {
|
|
1347
|
+
await git.checkoutBranch(currentBranch);
|
|
1348
|
+
return false;
|
|
1349
|
+
}
|
|
1350
|
+
const combinedPattern = patternsToAdd.join(" ");
|
|
1351
|
+
const addOk = await git.addFiles(combinedPattern);
|
|
1352
|
+
if (!addOk) {
|
|
1353
|
+
await git.checkoutBranch(currentBranch);
|
|
1354
|
+
return false;
|
|
1355
|
+
}
|
|
1356
|
+
const message = appendCiSkipTag(`docs(TASKS): task-${options.taskId} - done`, options.ciSkipTag);
|
|
1357
|
+
const commitOk = await git.commit(message);
|
|
1358
|
+
if (!commitOk) {
|
|
1359
|
+
await git.checkoutBranch(currentBranch);
|
|
1360
|
+
return false;
|
|
1361
|
+
}
|
|
1362
|
+
for (let attempt = 1; attempt <= MAX_RETRY_ATTEMPTS; attempt++) {
|
|
1363
|
+
if (attempt > 1) {
|
|
1364
|
+
await safeCall(() => git.fetch());
|
|
1365
|
+
await safeCall(() => git.rebase(`origin/${originBranch}`));
|
|
1366
|
+
}
|
|
1367
|
+
const pushOk = await safeCall(() => git.push(originBranch));
|
|
1368
|
+
if (pushOk === true) {
|
|
1369
|
+
await git.checkoutBranch(currentBranch);
|
|
1370
|
+
return true;
|
|
1371
|
+
}
|
|
1372
|
+
if (attempt >= MAX_RETRY_ATTEMPTS) {
|
|
1373
|
+
await git.checkoutBranch(currentBranch);
|
|
1374
|
+
throw new Error(`Squash push rejected after ${MAX_RETRY_ATTEMPTS} retries.`);
|
|
1375
|
+
}
|
|
1376
|
+
}
|
|
1377
|
+
await git.checkoutBranch(currentBranch);
|
|
1378
|
+
throw new Error(`Squash push rejected after ${MAX_RETRY_ATTEMPTS} retries.`);
|
|
1379
|
+
} catch (error2) {
|
|
1380
|
+
await git.checkoutBranch(currentBranch);
|
|
1381
|
+
if (error2 instanceof Error) {
|
|
1382
|
+
throw error2;
|
|
1383
|
+
}
|
|
1384
|
+
return false;
|
|
1385
|
+
}
|
|
1386
|
+
}
|
|
1387
|
+
var MAX_RETRY_ATTEMPTS;
|
|
1388
|
+
var init_auto_sync = __esm({
|
|
1389
|
+
"../file-system-task-provider/src/auto-sync.ts"() {
|
|
1390
|
+
"use strict";
|
|
1391
|
+
init_esm_shims();
|
|
1392
|
+
init_src();
|
|
1393
|
+
MAX_RETRY_ATTEMPTS = 3;
|
|
1394
|
+
}
|
|
1395
|
+
});
|
|
1396
|
+
|
|
1397
|
+
// ../file-system-task-provider/src/file-system-metrics-adapter.ts
|
|
1398
|
+
import {
|
|
1399
|
+
TASK_STATUSES,
|
|
1400
|
+
TASK_TYPES,
|
|
1401
|
+
UserStatsSchema
|
|
1402
|
+
} from "@opentask/taskin-types";
|
|
1403
|
+
import { promises as fs } from "fs";
|
|
1404
|
+
import path3 from "path";
|
|
1405
|
+
function toISOString(date) {
|
|
1406
|
+
return date.toISOString();
|
|
1407
|
+
}
|
|
1408
|
+
function emptyCodeMetrics() {
|
|
1409
|
+
return {
|
|
1410
|
+
linesAdded: 0,
|
|
1411
|
+
linesRemoved: 0,
|
|
1412
|
+
netChange: 0,
|
|
1413
|
+
characters: 0,
|
|
1414
|
+
filesChanged: 0,
|
|
1415
|
+
commits: 0
|
|
1416
|
+
};
|
|
1417
|
+
}
|
|
1418
|
+
function emptyTemporalMetrics() {
|
|
1419
|
+
return {
|
|
1420
|
+
byDayOfWeek: {
|
|
1421
|
+
"0": 0,
|
|
1422
|
+
"1": 0,
|
|
1423
|
+
"2": 0,
|
|
1424
|
+
"3": 0,
|
|
1425
|
+
"4": 0,
|
|
1426
|
+
"5": 0,
|
|
1427
|
+
"6": 0
|
|
1428
|
+
},
|
|
1429
|
+
byTimeOfDay: { morning: 0, afternoon: 0, evening: 0, night: 0 },
|
|
1430
|
+
streak: 0,
|
|
1431
|
+
trend: "stable"
|
|
1432
|
+
};
|
|
1433
|
+
}
|
|
1434
|
+
function removeCodeBlocks(content) {
|
|
1435
|
+
return content.replace(/```[\s\S]*?```/g, "");
|
|
1436
|
+
}
|
|
1437
|
+
function resolvePeriod(period = "week") {
|
|
1438
|
+
const now = /* @__PURE__ */ new Date();
|
|
1439
|
+
const until = now;
|
|
1440
|
+
let since;
|
|
1441
|
+
switch (period) {
|
|
1442
|
+
case "day":
|
|
1443
|
+
since = new Date(Date.now() - MILLISECONDS_PER_DAY);
|
|
1444
|
+
break;
|
|
1445
|
+
case "week":
|
|
1446
|
+
since = new Date(Date.now() - DAYS_PER_WEEK * MILLISECONDS_PER_DAY);
|
|
1447
|
+
break;
|
|
1448
|
+
case "month":
|
|
1449
|
+
since = new Date(Date.now() - 30 * MILLISECONDS_PER_DAY);
|
|
1450
|
+
break;
|
|
1451
|
+
case "quarter":
|
|
1452
|
+
since = new Date(Date.now() - 90 * MILLISECONDS_PER_DAY);
|
|
1453
|
+
break;
|
|
1454
|
+
case "year":
|
|
1455
|
+
since = new Date(Date.now() - 365 * MILLISECONDS_PER_DAY);
|
|
1456
|
+
break;
|
|
1457
|
+
case "all":
|
|
1458
|
+
since = /* @__PURE__ */ new Date(0);
|
|
1459
|
+
break;
|
|
1460
|
+
default:
|
|
1461
|
+
since = new Date(Date.now() - DAYS_PER_WEEK * MILLISECONDS_PER_DAY);
|
|
1462
|
+
}
|
|
1463
|
+
return { since, until };
|
|
1464
|
+
}
|
|
1465
|
+
function calculateActivityFrequency(commits, period) {
|
|
1466
|
+
const daysInPeriod = {
|
|
1467
|
+
day: 1,
|
|
1468
|
+
week: 7,
|
|
1469
|
+
month: 30,
|
|
1470
|
+
quarter: 90,
|
|
1471
|
+
year: 365,
|
|
1472
|
+
all: 365
|
|
1473
|
+
// Use 1 year as baseline for 'all'
|
|
1474
|
+
};
|
|
1475
|
+
return commits / daysInPeriod[period];
|
|
1476
|
+
}
|
|
1477
|
+
async function calculateCodeMetrics(gitAnalyzer, username, since, until) {
|
|
1478
|
+
if (!gitAnalyzer) {
|
|
1479
|
+
return emptyCodeMetrics();
|
|
759
1480
|
}
|
|
760
1481
|
try {
|
|
761
1482
|
const commits = await gitAnalyzer.getCommits({
|
|
@@ -1116,56 +1837,6 @@ var init_file_system_metrics_adapter = __esm({
|
|
|
1116
1837
|
}
|
|
1117
1838
|
});
|
|
1118
1839
|
|
|
1119
|
-
// ../file-system-task-provider/src/i18n.ts
|
|
1120
|
-
function getI18n(locale = "en-US") {
|
|
1121
|
-
return i18nConfig[locale];
|
|
1122
|
-
}
|
|
1123
|
-
function detectLocale(content) {
|
|
1124
|
-
if (content.includes("## Descri\xE7\xE3o") || content.includes("## Tipo") || content.includes("## Respons\xE1vel") || content.includes("## Tarefas")) {
|
|
1125
|
-
return "pt-BR";
|
|
1126
|
-
}
|
|
1127
|
-
return "en-US";
|
|
1128
|
-
}
|
|
1129
|
-
var i18nConfig;
|
|
1130
|
-
var init_i18n = __esm({
|
|
1131
|
-
"../file-system-task-provider/src/i18n.ts"() {
|
|
1132
|
-
"use strict";
|
|
1133
|
-
init_esm_shims();
|
|
1134
|
-
i18nConfig = {
|
|
1135
|
-
"en-US": {
|
|
1136
|
-
status: "Status",
|
|
1137
|
-
type: "Type",
|
|
1138
|
-
assignee: "Assignee",
|
|
1139
|
-
description: "Description",
|
|
1140
|
-
tasks: "Tasks",
|
|
1141
|
-
notes: "Notes",
|
|
1142
|
-
defaultAssignee: "To be defined",
|
|
1143
|
-
descriptionPlaceholder: "Add task description here...",
|
|
1144
|
-
notesPlaceholder: "Add any relevant notes or links here.",
|
|
1145
|
-
priority: "Priority",
|
|
1146
|
-
group: "Group",
|
|
1147
|
-
groupName: "GroupName",
|
|
1148
|
-
difficulty: "Difficulty"
|
|
1149
|
-
},
|
|
1150
|
-
"pt-BR": {
|
|
1151
|
-
status: "Status",
|
|
1152
|
-
type: "Tipo",
|
|
1153
|
-
assignee: "Respons\xE1vel",
|
|
1154
|
-
description: "Descri\xE7\xE3o",
|
|
1155
|
-
tasks: "Tarefas",
|
|
1156
|
-
notes: "Notas",
|
|
1157
|
-
defaultAssignee: "A definir",
|
|
1158
|
-
descriptionPlaceholder: "Adicione a descri\xE7\xE3o da tarefa aqui...",
|
|
1159
|
-
notesPlaceholder: "Adicione notas ou links relevantes aqui.",
|
|
1160
|
-
priority: "Prioridade",
|
|
1161
|
-
group: "Grupo",
|
|
1162
|
-
groupName: "NomeGrupo",
|
|
1163
|
-
difficulty: "Dificuldade"
|
|
1164
|
-
}
|
|
1165
|
-
};
|
|
1166
|
-
}
|
|
1167
|
-
});
|
|
1168
|
-
|
|
1169
1840
|
// ../file-system-task-provider/src/task-validator.ts
|
|
1170
1841
|
var task_validator_exports = {};
|
|
1171
1842
|
__export(task_validator_exports, {
|
|
@@ -1343,7 +2014,7 @@ var init_task_validator = __esm({
|
|
|
1343
2014
|
});
|
|
1344
2015
|
|
|
1345
2016
|
// ../file-system-task-provider/src/user-registry.ts
|
|
1346
|
-
import { execSync } from "child_process";
|
|
2017
|
+
import { execSync as execSync3 } from "child_process";
|
|
1347
2018
|
import { createHash } from "crypto";
|
|
1348
2019
|
import { promises as fs2 } from "fs";
|
|
1349
2020
|
import path4 from "path";
|
|
@@ -1460,7 +2131,7 @@ var init_user_registry = __esm({
|
|
|
1460
2131
|
}
|
|
1461
2132
|
readGitConfig(key) {
|
|
1462
2133
|
try {
|
|
1463
|
-
return
|
|
2134
|
+
return execSync3(`git config ${key}`, {
|
|
1464
2135
|
encoding: "utf-8",
|
|
1465
2136
|
stdio: "pipe"
|
|
1466
2137
|
}).trim();
|
|
@@ -1636,7 +2307,7 @@ var init_file_system_task_provider = __esm({
|
|
|
1636
2307
|
"../file-system-task-provider/src/file-system-task-provider.ts"() {
|
|
1637
2308
|
"use strict";
|
|
1638
2309
|
init_esm_shims();
|
|
1639
|
-
|
|
2310
|
+
init_src2();
|
|
1640
2311
|
init_assignee_identity();
|
|
1641
2312
|
init_i18n();
|
|
1642
2313
|
init_metadata_style2();
|
|
@@ -2045,7 +2716,7 @@ __export(src_exports, {
|
|
|
2045
2716
|
validateUsersFileLocation: () => validateUsersFileLocation,
|
|
2046
2717
|
writeMetadataField: () => writeMetadataField
|
|
2047
2718
|
});
|
|
2048
|
-
var
|
|
2719
|
+
var init_src3 = __esm({
|
|
2049
2720
|
"../file-system-task-provider/src/index.ts"() {
|
|
2050
2721
|
"use strict";
|
|
2051
2722
|
init_esm_shims();
|
|
@@ -2068,6 +2739,7 @@ import { Command } from "commander";
|
|
|
2068
2739
|
|
|
2069
2740
|
// src/commands/config.ts
|
|
2070
2741
|
init_esm_shims();
|
|
2742
|
+
init_src();
|
|
2071
2743
|
import chalk2 from "chalk";
|
|
2072
2744
|
import inquirer from "inquirer";
|
|
2073
2745
|
|
|
@@ -2114,9 +2786,15 @@ function warning(message) {
|
|
|
2114
2786
|
|
|
2115
2787
|
// src/lib/config-manager.ts
|
|
2116
2788
|
init_esm_shims();
|
|
2789
|
+
init_src();
|
|
2117
2790
|
import { TaskinConfigSchema } from "@opentask/taskin-types";
|
|
2118
2791
|
import { existsSync, readFileSync, writeFileSync } from "fs";
|
|
2119
2792
|
import { join } from "path";
|
|
2793
|
+
var DEFAULT_AUTOMATION_CONFIG = {
|
|
2794
|
+
level: "assisted",
|
|
2795
|
+
autoSync: true,
|
|
2796
|
+
ciSkipTag: DEFAULT_CI_SKIP_TAG
|
|
2797
|
+
};
|
|
2120
2798
|
function getAutomationBehavior(level, commits) {
|
|
2121
2799
|
const presets = {
|
|
2122
2800
|
manual: {
|
|
@@ -2170,6 +2848,9 @@ ${result.error.message}`);
|
|
|
2170
2848
|
}
|
|
2171
2849
|
/**
|
|
2172
2850
|
* Save configuration to .taskin.json
|
|
2851
|
+
*
|
|
2852
|
+
* Takes the *input* shape: the schema fills in what it has defaults for, so
|
|
2853
|
+
* a caller need not spell out `automation.ciSkipTag` to save a config.
|
|
2173
2854
|
*/
|
|
2174
2855
|
saveConfig(config) {
|
|
2175
2856
|
const validated = TaskinConfigSchema.parse(config);
|
|
@@ -2193,7 +2874,7 @@ ${result.error.message}`);
|
|
|
2193
2874
|
setAutomationLevel(level) {
|
|
2194
2875
|
const config = this.loadConfig();
|
|
2195
2876
|
config.automation = {
|
|
2196
|
-
|
|
2877
|
+
...DEFAULT_AUTOMATION_CONFIG,
|
|
2197
2878
|
...config.automation,
|
|
2198
2879
|
level
|
|
2199
2880
|
};
|
|
@@ -2205,11 +2886,33 @@ ${result.error.message}`);
|
|
|
2205
2886
|
getAutomationConfig() {
|
|
2206
2887
|
try {
|
|
2207
2888
|
const config = this.loadConfig();
|
|
2208
|
-
return config.automation ??
|
|
2889
|
+
return config.automation ?? DEFAULT_AUTOMATION_CONFIG;
|
|
2209
2890
|
} catch {
|
|
2210
|
-
return
|
|
2891
|
+
return DEFAULT_AUTOMATION_CONFIG;
|
|
2211
2892
|
}
|
|
2212
2893
|
}
|
|
2894
|
+
/**
|
|
2895
|
+
* Tag appended to the commits Taskin writes on its own, so a status change
|
|
2896
|
+
* does not trigger the project's pipeline.
|
|
2897
|
+
*
|
|
2898
|
+
* Returns `[skip ci]` when unconfigured. An empty string is a real answer —
|
|
2899
|
+
* it means the project wants CI to run — so it is returned as-is.
|
|
2900
|
+
*/
|
|
2901
|
+
getCiSkipTag() {
|
|
2902
|
+
return this.getAutomationConfig().ciSkipTag ?? DEFAULT_CI_SKIP_TAG;
|
|
2903
|
+
}
|
|
2904
|
+
/**
|
|
2905
|
+
* Set the CI-skip tag, preserving the rest of the automation block.
|
|
2906
|
+
*/
|
|
2907
|
+
setCiSkipTag(ciSkipTag) {
|
|
2908
|
+
const config = this.loadConfig();
|
|
2909
|
+
config.automation = {
|
|
2910
|
+
...DEFAULT_AUTOMATION_CONFIG,
|
|
2911
|
+
...config.automation,
|
|
2912
|
+
ciSkipTag
|
|
2913
|
+
};
|
|
2914
|
+
this.saveConfig(config);
|
|
2915
|
+
}
|
|
2213
2916
|
/**
|
|
2214
2917
|
* Set automation configuration
|
|
2215
2918
|
*/
|
|
@@ -2227,7 +2930,8 @@ ${result.error.message}`);
|
|
|
2227
2930
|
...getAutomationBehavior(automation.level, automation.commits),
|
|
2228
2931
|
defaultBranch: automation.defaultBranch,
|
|
2229
2932
|
autoSync: automation.autoSync,
|
|
2230
|
-
originBranch: automation.originBranch
|
|
2933
|
+
originBranch: automation.originBranch,
|
|
2934
|
+
ciSkipTag: automation.ciSkipTag ?? DEFAULT_CI_SKIP_TAG
|
|
2231
2935
|
};
|
|
2232
2936
|
}
|
|
2233
2937
|
/**
|
|
@@ -2339,6 +3043,14 @@ var defineCommand = (config) => {
|
|
|
2339
3043
|
init_esm_shims();
|
|
2340
3044
|
|
|
2341
3045
|
// src/commands/config.ts
|
|
3046
|
+
var NO_CI_SKIP_TAG_KEYWORD = "none";
|
|
3047
|
+
function normalizeCiSkipTagInput(input) {
|
|
3048
|
+
const trimmed = input.trim();
|
|
3049
|
+
return trimmed.toLowerCase() === NO_CI_SKIP_TAG_KEYWORD ? "" : trimmed;
|
|
3050
|
+
}
|
|
3051
|
+
function describeCiSkipTag(tag) {
|
|
3052
|
+
return tag.length === 0 ? "no tag \u2014 CI runs on status commits" : tag;
|
|
3053
|
+
}
|
|
2342
3054
|
var configCommand = defineCommand({
|
|
2343
3055
|
name: "config",
|
|
2344
3056
|
description: "\u2699\uFE0F Configure Taskin settings",
|
|
@@ -2358,6 +3070,10 @@ var configCommand = defineCommand({
|
|
|
2358
3070
|
{
|
|
2359
3071
|
flags: "--notification-events <events>",
|
|
2360
3072
|
description: "Comma-separated events (task:start,task:done,task:review)"
|
|
3073
|
+
},
|
|
3074
|
+
{
|
|
3075
|
+
flags: "--ci-skip-tag <tag>",
|
|
3076
|
+
description: `Tag appended to Taskin's own commits so they skip CI (default "${DEFAULT_CI_SKIP_TAG}"; "none" to run CI)`
|
|
2361
3077
|
}
|
|
2362
3078
|
],
|
|
2363
3079
|
handler: async (options) => {
|
|
@@ -2375,8 +3091,12 @@ async function handleConfigCommand(options) {
|
|
|
2375
3091
|
await setAutomationLevel(configManager, options.level);
|
|
2376
3092
|
return;
|
|
2377
3093
|
}
|
|
2378
|
-
if (options
|
|
2379
|
-
|
|
3094
|
+
if (options.ciSkipTag !== void 0) {
|
|
3095
|
+
setCiSkipTag(configManager, options.ciSkipTag);
|
|
3096
|
+
return;
|
|
3097
|
+
}
|
|
3098
|
+
if (options.discordWebhook) {
|
|
3099
|
+
await setDiscordNotification(configManager, options.discordWebhook, options.notificationEvents);
|
|
2380
3100
|
return;
|
|
2381
3101
|
}
|
|
2382
3102
|
await interactiveConfig(configManager);
|
|
@@ -2397,7 +3117,8 @@ async function showConfiguration(configManager) {
|
|
|
2397
3117
|
` Auto-commit status changes: ${behavior.autoCommitStatusChange ? chalk2.green("\u2713 Yes") : chalk2.red("\u2717 No")}`
|
|
2398
3118
|
);
|
|
2399
3119
|
console.log(` Auto-commit on pause: ${behavior.autoCommitPause ? chalk2.green("\u2713 Yes") : chalk2.red("\u2717 No")}`);
|
|
2400
|
-
console.log(` Auto-commit on finish: ${behavior.autoCommitFinish ? chalk2.green("\u2713 Yes") : chalk2.red("\u2717 No")}
|
|
3120
|
+
console.log(` Auto-commit on finish: ${behavior.autoCommitFinish ? chalk2.green("\u2713 Yes") : chalk2.red("\u2717 No")}`);
|
|
3121
|
+
console.log(` CI skip tag: ${chalk2.cyan(describeCiSkipTag(configManager.getCiSkipTag()))}
|
|
2401
3122
|
`);
|
|
2402
3123
|
console.log(chalk2.bold("\u{1F514} Notifications"));
|
|
2403
3124
|
const notifications = configManager.getNotifications();
|
|
@@ -2432,6 +3153,24 @@ async function showConfiguration(configManager) {
|
|
|
2432
3153
|
process.exit(1);
|
|
2433
3154
|
}
|
|
2434
3155
|
}
|
|
3156
|
+
function warnAboutUnrecognizedCiSkipTag(tag) {
|
|
3157
|
+
if (tag.length === 0 || isRecognizedCiSkipTag(tag)) return;
|
|
3158
|
+
console.log();
|
|
3159
|
+
console.log(chalk2.yellow("\u26A0\uFE0F This tag is not one GitHub, GitLab or Bitbucket documents."));
|
|
3160
|
+
if (tag.replace(/\s+/g, "").toLowerCase() === "[skip-ci]") {
|
|
3161
|
+
console.log(chalk2.dim(' "[skip-ci]" with a hyphen is not recognized anywhere \u2014 it triggers CI.'));
|
|
3162
|
+
console.log(chalk2.dim(' Did you mean "[skip ci]", with a space?'));
|
|
3163
|
+
}
|
|
3164
|
+
console.log(chalk2.dim(` Documented tags: ${CI_SKIP_TAGS.join(", ")}`));
|
|
3165
|
+
console.log(chalk2.dim(" Keeping it anyway \u2014 a self-hosted pipeline can match whatever it likes."));
|
|
3166
|
+
}
|
|
3167
|
+
function setCiSkipTag(configManager, rawTag) {
|
|
3168
|
+
printHeader("Configure CI Skip Tag", "\u2699\uFE0F");
|
|
3169
|
+
const tag = normalizeCiSkipTagInput(rawTag);
|
|
3170
|
+
configManager.setCiSkipTag(tag);
|
|
3171
|
+
success(`CI skip tag set to ${colors.highlight(describeCiSkipTag(tag))}`);
|
|
3172
|
+
warnAboutUnrecognizedCiSkipTag(tag);
|
|
3173
|
+
}
|
|
2435
3174
|
async function setDiscordNotification(configManager, webhookUrl, eventsFlag) {
|
|
2436
3175
|
printHeader("Configure Discord Notification", "\u{1F514}");
|
|
2437
3176
|
const events = eventsFlag ? eventsFlag.split(",").map((e) => e.trim()) : ["task:start", "task:done"];
|
|
@@ -2487,6 +3226,7 @@ async function interactiveConfig(configManager) {
|
|
|
2487
3226
|
message: "What would you like to configure?",
|
|
2488
3227
|
choices: [
|
|
2489
3228
|
{ name: "\u{1F916} Automation level", value: "automation" },
|
|
3229
|
+
{ name: "\u23ED\uFE0F CI skip tag", value: "ciSkipTag" },
|
|
2490
3230
|
{ name: "\u{1F514} Discord notification", value: "discord" },
|
|
2491
3231
|
{ name: "\u{1F514} Telegram notification", value: "telegram" }
|
|
2492
3232
|
]
|
|
@@ -2494,6 +3234,8 @@ async function interactiveConfig(configManager) {
|
|
|
2494
3234
|
]);
|
|
2495
3235
|
if (section === "automation") {
|
|
2496
3236
|
await configureAutomation(configManager);
|
|
3237
|
+
} else if (section === "ciSkipTag") {
|
|
3238
|
+
await configureCiSkipTag(configManager);
|
|
2497
3239
|
} else if (section === "discord") {
|
|
2498
3240
|
await configureDiscordNotification(configManager);
|
|
2499
3241
|
} else if (section === "telegram") {
|
|
@@ -2545,6 +3287,40 @@ async function configureAutomation(configManager) {
|
|
|
2545
3287
|
console.log(chalk2.dim(` Auto-commit on pause: ${behavior.autoCommitPause ? "\u2713" : "\u2717"}`));
|
|
2546
3288
|
console.log(chalk2.dim(` Auto-commit on finish: ${behavior.autoCommitFinish ? "\u2713" : "\u2717"}`));
|
|
2547
3289
|
}
|
|
3290
|
+
async function configureCiSkipTag(configManager) {
|
|
3291
|
+
printHeader("Configure CI Skip Tag", "\u23ED\uFE0F");
|
|
3292
|
+
const current = configManager.getCiSkipTag();
|
|
3293
|
+
console.log(`Current tag: ${chalk2.cyan(describeCiSkipTag(current))}
|
|
3294
|
+
`);
|
|
3295
|
+
console.log(chalk2.dim("Taskin appends this to the commits it writes itself \u2014 status changes and"));
|
|
3296
|
+
console.log(chalk2.dim("task files \u2014 so they do not trigger your pipeline.\n"));
|
|
3297
|
+
const { choice } = await inquirer.prompt([
|
|
3298
|
+
{
|
|
3299
|
+
type: "list",
|
|
3300
|
+
name: "choice",
|
|
3301
|
+
message: "Tag for Taskin commits:",
|
|
3302
|
+
default: current.length === 0 ? NO_CI_SKIP_TAG_KEYWORD : current,
|
|
3303
|
+
choices: [
|
|
3304
|
+
{ name: `${CI_SKIP_TAGS[0]} \u2014 GitHub, GitLab and Bitbucket (recommended)`, value: CI_SKIP_TAGS[0] },
|
|
3305
|
+
{ name: `${CI_SKIP_TAGS[1]} \u2014 GitHub, GitLab and Bitbucket`, value: CI_SKIP_TAGS[1] },
|
|
3306
|
+
{ name: `${CI_SKIP_TAGS[2]} \u2014 GitHub Actions only`, value: CI_SKIP_TAGS[2] },
|
|
3307
|
+
{ name: `${CI_SKIP_TAGS[3]} \u2014 GitHub Actions only`, value: CI_SKIP_TAGS[3] },
|
|
3308
|
+
{ name: `${CI_SKIP_TAGS[4]} \u2014 GitHub Actions only`, value: CI_SKIP_TAGS[4] },
|
|
3309
|
+
{ name: "none \u2014 do not mark the commits, let CI run", value: NO_CI_SKIP_TAG_KEYWORD },
|
|
3310
|
+
{ name: "custom\u2026 \u2014 another CI (Azure DevOps uses ***NO_CI***)", value: "custom" }
|
|
3311
|
+
]
|
|
3312
|
+
}
|
|
3313
|
+
]);
|
|
3314
|
+
const tag = choice === "custom" ? (await inquirer.prompt([
|
|
3315
|
+
{
|
|
3316
|
+
type: "input",
|
|
3317
|
+
name: "customTag",
|
|
3318
|
+
message: "Tag to append:",
|
|
3319
|
+
default: current
|
|
3320
|
+
}
|
|
3321
|
+
])).customTag : choice;
|
|
3322
|
+
setCiSkipTag(configManager, tag);
|
|
3323
|
+
}
|
|
2548
3324
|
async function configureDiscordNotification(configManager) {
|
|
2549
3325
|
printHeader("Configure Discord Notification", "\u{1F514}");
|
|
2550
3326
|
const notifications = configManager.getNotifications() ?? {};
|
|
@@ -3000,1220 +3776,640 @@ var TaskWebSocketServer = class {
|
|
|
3000
3776
|
await this.handlePauseRequest(client, message);
|
|
3001
3777
|
break;
|
|
3002
3778
|
case "ping":
|
|
3003
|
-
this.sendToClient(client.id, { type: "pong" });
|
|
3004
|
-
break;
|
|
3005
|
-
default:
|
|
3006
|
-
this.sendToClient(client.id, {
|
|
3007
|
-
type: "error",
|
|
3008
|
-
payload: { message: `Unknown message type: ${message.type}` }
|
|
3009
|
-
});
|
|
3010
|
-
}
|
|
3011
|
-
} catch (error2) {
|
|
3012
|
-
this.log("Error handling message:", error2);
|
|
3013
|
-
this.sendToClient(client.id, {
|
|
3014
|
-
type: "error",
|
|
3015
|
-
payload: {
|
|
3016
|
-
message: error2 instanceof Error ? error2.message : "Failed to process message"
|
|
3017
|
-
}
|
|
3018
|
-
});
|
|
3019
|
-
}
|
|
3020
|
-
}
|
|
3021
|
-
/**
|
|
3022
|
-
* Reads the task id out of an incoming message.
|
|
3023
|
-
*
|
|
3024
|
-
* Ids on the wire are untrusted strings: a branded `TaskId` is only worth
|
|
3025
|
-
* something if the boundary that produces it actually validates. Answers the
|
|
3026
|
-
* client with a clear error instead of letting a ZodError leak out of the
|
|
3027
|
-
* generic catch.
|
|
3028
|
-
*/
|
|
3029
|
-
readTaskId(client, message, field = "taskId") {
|
|
3030
|
-
const raw = message.payload?.[field];
|
|
3031
|
-
const parsed = typeof raw === "string" ? TaskIdSchema.safeParse(raw) : void 0;
|
|
3032
|
-
if (!parsed?.success) {
|
|
3033
|
-
this.sendToClient(client.id, {
|
|
3034
|
-
type: "error",
|
|
3035
|
-
payload: { message: `Invalid task id in '${message.type}' request` },
|
|
3036
|
-
requestId: message.requestId
|
|
3037
|
-
});
|
|
3038
|
-
return void 0;
|
|
3039
|
-
}
|
|
3040
|
-
return parsed.data;
|
|
3041
|
-
}
|
|
3042
|
-
/**
|
|
3043
|
-
* Handle list request
|
|
3044
|
-
*/
|
|
3045
|
-
async handleListRequest(client, message) {
|
|
3046
|
-
const tasks = await this.taskProvider.getAllTasks();
|
|
3047
|
-
const [first] = tasks;
|
|
3048
|
-
if (first) {
|
|
3049
|
-
this.log("[WS Server] Sending", tasks.length, "tasks");
|
|
3050
|
-
this.log("[WS Server] First task assignee:", first.assignee);
|
|
3051
|
-
}
|
|
3052
|
-
this.sendToClient(client.id, {
|
|
3053
|
-
type: "tasks",
|
|
3054
|
-
payload: tasks,
|
|
3055
|
-
requestId: message.requestId
|
|
3056
|
-
});
|
|
3057
|
-
}
|
|
3058
|
-
/**
|
|
3059
|
-
* Handle find request
|
|
3060
|
-
*/
|
|
3061
|
-
async handleFindRequest(client, message) {
|
|
3062
|
-
const taskId = this.readTaskId(client, message);
|
|
3063
|
-
if (!taskId)
|
|
3064
|
-
return;
|
|
3065
|
-
const task = await this.taskProvider.findTask(taskId);
|
|
3066
|
-
this.sendToClient(client.id, {
|
|
3067
|
-
type: "task:found",
|
|
3068
|
-
payload: task,
|
|
3069
|
-
requestId: message.requestId
|
|
3070
|
-
});
|
|
3071
|
-
}
|
|
3072
|
-
/**
|
|
3073
|
-
* Handle update request
|
|
3074
|
-
*/
|
|
3075
|
-
async handleUpdateRequest(client, message) {
|
|
3076
|
-
const taskId = this.readTaskId(client, message, "id");
|
|
3077
|
-
if (!taskId)
|
|
3078
|
-
return;
|
|
3079
|
-
const stored = await this.taskProvider.findTask(taskId);
|
|
3080
|
-
if (!stored) {
|
|
3081
|
-
this.sendToClient(client.id, {
|
|
3082
|
-
type: "error",
|
|
3083
|
-
payload: { message: `Task ${taskId} not found` },
|
|
3084
|
-
requestId: message.requestId
|
|
3085
|
-
});
|
|
3086
|
-
return;
|
|
3087
|
-
}
|
|
3088
|
-
const outcome = applyTaskUpdate(stored, message.payload);
|
|
3089
|
-
if (!outcome.ok) {
|
|
3090
|
-
this.sendToClient(client.id, {
|
|
3091
|
-
type: "error",
|
|
3092
|
-
payload: { message: outcome.message },
|
|
3093
|
-
requestId: message.requestId
|
|
3094
|
-
});
|
|
3095
|
-
return;
|
|
3096
|
-
}
|
|
3097
|
-
await this.taskProvider.updateTask(outcome.task);
|
|
3098
|
-
this.broadcast({
|
|
3099
|
-
type: "task:updated",
|
|
3100
|
-
payload: outcome.task
|
|
3101
|
-
});
|
|
3102
|
-
}
|
|
3103
|
-
/**
|
|
3104
|
-
* Handle start task request
|
|
3105
|
-
*/
|
|
3106
|
-
async handleStartRequest(client, message) {
|
|
3107
|
-
const taskId = this.readTaskId(client, message);
|
|
3108
|
-
if (!taskId)
|
|
3109
|
-
return;
|
|
3110
|
-
const task = await this.taskManager.startTask(taskId);
|
|
3111
|
-
this.broadcast({
|
|
3112
|
-
type: "task:updated",
|
|
3113
|
-
payload: task
|
|
3114
|
-
});
|
|
3115
|
-
}
|
|
3116
|
-
/**
|
|
3117
|
-
* Handle finish task request
|
|
3118
|
-
*/
|
|
3119
|
-
async handleFinishRequest(client, message) {
|
|
3120
|
-
const taskId = this.readTaskId(client, message);
|
|
3121
|
-
if (!taskId)
|
|
3122
|
-
return;
|
|
3123
|
-
const task = await this.taskManager.finishTask(taskId);
|
|
3124
|
-
this.broadcast({
|
|
3125
|
-
type: "task:updated",
|
|
3126
|
-
payload: task
|
|
3127
|
-
});
|
|
3128
|
-
}
|
|
3129
|
-
/**
|
|
3130
|
-
* Handle pause task request
|
|
3131
|
-
*/
|
|
3132
|
-
async handlePauseRequest(client, message) {
|
|
3133
|
-
const taskId = this.readTaskId(client, message);
|
|
3134
|
-
if (!taskId)
|
|
3135
|
-
return;
|
|
3136
|
-
const task = await this.taskManager.pauseTask(taskId);
|
|
3137
|
-
this.broadcast({
|
|
3138
|
-
type: "task:updated",
|
|
3139
|
-
payload: task
|
|
3140
|
-
});
|
|
3141
|
-
}
|
|
3142
|
-
/**
|
|
3143
|
-
* Start heartbeat to check client connections
|
|
3144
|
-
*/
|
|
3145
|
-
startHeartbeat() {
|
|
3146
|
-
this.heartbeatInterval = setInterval(() => {
|
|
3147
|
-
this.clients.forEach((client) => {
|
|
3148
|
-
if (!client.isAlive) {
|
|
3149
|
-
this.log(`Client ${client.id} timeout, terminating`);
|
|
3150
|
-
client.ws.terminate();
|
|
3151
|
-
this.clients.delete(client.id);
|
|
3152
|
-
return;
|
|
3153
|
-
}
|
|
3154
|
-
client.isAlive = false;
|
|
3155
|
-
client.ws.ping();
|
|
3156
|
-
});
|
|
3157
|
-
}, this.options.heartbeatInterval);
|
|
3158
|
-
}
|
|
3159
|
-
/**
|
|
3160
|
-
* Stop heartbeat
|
|
3161
|
-
*/
|
|
3162
|
-
stopHeartbeat() {
|
|
3163
|
-
if (this.heartbeatInterval) {
|
|
3164
|
-
clearInterval(this.heartbeatInterval);
|
|
3165
|
-
this.heartbeatInterval = null;
|
|
3166
|
-
}
|
|
3167
|
-
}
|
|
3168
|
-
/**
|
|
3169
|
-
* Debug logging
|
|
3170
|
-
*/
|
|
3171
|
-
log(...args) {
|
|
3172
|
-
if (this.options.debug) {
|
|
3173
|
-
console.log("[TaskWebSocketServer]", ...args);
|
|
3174
|
-
}
|
|
3175
|
-
}
|
|
3176
|
-
};
|
|
3177
|
-
|
|
3178
|
-
// node_modules/@opentask/taskin-task-server-ws/dist/task-server-ws.mock.js
|
|
3179
|
-
init_esm_shims();
|
|
3180
|
-
import { parseTaskId } from "@opentask/taskin-types";
|
|
3181
|
-
|
|
3182
|
-
// node_modules/@opentask/taskin-task-server-ws/dist/task-server-ws.types.js
|
|
3183
|
-
init_esm_shims();
|
|
3184
|
-
|
|
3185
|
-
// src/commands/dashboard.ts
|
|
3186
|
-
init_src();
|
|
3187
|
-
import chalk4 from "chalk";
|
|
3188
|
-
import express from "express";
|
|
3189
|
-
import { createServer } from "http";
|
|
3190
|
-
import path8 from "path";
|
|
3191
|
-
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
3192
|
-
|
|
3193
|
-
// src/lib/provider-factory/index.ts
|
|
3194
|
-
init_esm_shims();
|
|
3195
|
-
|
|
3196
|
-
// src/lib/provider-factory/provider-factory.ts
|
|
3197
|
-
init_esm_shims();
|
|
3198
|
-
import path7 from "path";
|
|
3199
|
-
|
|
3200
|
-
// src/lib/notification/env-resolver.ts
|
|
3201
|
-
init_esm_shims();
|
|
3202
|
-
import { readFileSync as readFileSync2 } from "fs";
|
|
3203
|
-
import { join as join2 } from "path";
|
|
3204
|
-
var ENV_VAR_PATTERN = /\$\{([^}]+)\}/g;
|
|
3205
|
-
function loadDotEnv(dir) {
|
|
3206
|
-
try {
|
|
3207
|
-
const envPath = join2(dir ?? process.cwd(), ".env");
|
|
3208
|
-
const content = readFileSync2(envPath, "utf-8");
|
|
3209
|
-
for (const line of content.split("\n")) {
|
|
3210
|
-
const trimmed = line.trim();
|
|
3211
|
-
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
3212
|
-
const eqIndex = trimmed.indexOf("=");
|
|
3213
|
-
if (eqIndex === -1) continue;
|
|
3214
|
-
const key = trimmed.slice(0, eqIndex).trim();
|
|
3215
|
-
let value = trimmed.slice(eqIndex + 1).trim();
|
|
3216
|
-
if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) {
|
|
3217
|
-
value = value.slice(1, -1);
|
|
3218
|
-
}
|
|
3219
|
-
if (!process.env[key]) {
|
|
3220
|
-
process.env[key] = value;
|
|
3221
|
-
}
|
|
3222
|
-
}
|
|
3223
|
-
} catch {
|
|
3224
|
-
}
|
|
3225
|
-
}
|
|
3226
|
-
function resolveEnvVars(value) {
|
|
3227
|
-
return value.replace(ENV_VAR_PATTERN, (_match, varName) => {
|
|
3228
|
-
return process.env[varName] ?? "";
|
|
3229
|
-
});
|
|
3230
|
-
}
|
|
3231
|
-
|
|
3232
|
-
// src/lib/provider-registry/index.ts
|
|
3233
|
-
init_esm_shims();
|
|
3234
|
-
|
|
3235
|
-
// src/lib/provider-registry/provider-registry.ts
|
|
3236
|
-
init_esm_shims();
|
|
3237
|
-
var AVAILABLE_PROVIDERS = [
|
|
3238
|
-
{
|
|
3239
|
-
id: "fs",
|
|
3240
|
-
name: "\u{1F4C1} File System",
|
|
3241
|
-
description: "Store tasks as Markdown files in a local TASKS/ directory",
|
|
3242
|
-
packageName: "@opentask/taskin-file-system-provider",
|
|
3243
|
-
configSchema: {
|
|
3244
|
-
required: ["tasksDir"],
|
|
3245
|
-
properties: {
|
|
3246
|
-
tasksDir: {
|
|
3247
|
-
type: "string",
|
|
3248
|
-
description: "Directory to store task files"
|
|
3249
|
-
},
|
|
3250
|
-
metadataStyle: {
|
|
3251
|
-
type: "string",
|
|
3252
|
-
description: "Marking of the metadata block for new files (list | hard-break | plain)"
|
|
3253
|
-
}
|
|
3254
|
-
}
|
|
3255
|
-
},
|
|
3256
|
-
status: "stable"
|
|
3257
|
-
},
|
|
3258
|
-
{
|
|
3259
|
-
id: "redmine",
|
|
3260
|
-
name: "\u{1F534} Redmine",
|
|
3261
|
-
description: "Sync tasks with Redmine issues via REST API",
|
|
3262
|
-
packageName: "@opentask/taskin-redmine-provider",
|
|
3263
|
-
configSchema: {
|
|
3264
|
-
required: ["apiUrl", "apiKey", "projectId"],
|
|
3265
|
-
properties: {
|
|
3266
|
-
apiUrl: {
|
|
3267
|
-
type: "string",
|
|
3268
|
-
description: "Redmine server URL (e.g., https://redmine.example.com)"
|
|
3269
|
-
},
|
|
3270
|
-
apiKey: {
|
|
3271
|
-
type: "string",
|
|
3272
|
-
description: "Your Redmine API key",
|
|
3273
|
-
secret: true
|
|
3274
|
-
},
|
|
3275
|
-
projectId: {
|
|
3276
|
-
type: "string",
|
|
3277
|
-
description: "Project identifier or ID"
|
|
3278
|
-
}
|
|
3279
|
-
}
|
|
3280
|
-
},
|
|
3281
|
-
status: "coming-soon"
|
|
3282
|
-
},
|
|
3283
|
-
{
|
|
3284
|
-
id: "jira",
|
|
3285
|
-
name: "\u{1F535} Jira",
|
|
3286
|
-
description: "Sync tasks with Jira issues via REST API",
|
|
3287
|
-
packageName: "@opentask/taskin-jira-provider",
|
|
3288
|
-
configSchema: {
|
|
3289
|
-
required: ["apiUrl", "email", "apiToken", "projectKey"],
|
|
3290
|
-
properties: {
|
|
3291
|
-
apiUrl: {
|
|
3292
|
-
type: "string",
|
|
3293
|
-
description: "Jira server URL (e.g., https://company.atlassian.net)"
|
|
3294
|
-
},
|
|
3295
|
-
email: {
|
|
3296
|
-
type: "string",
|
|
3297
|
-
description: "Your Atlassian account email"
|
|
3298
|
-
},
|
|
3299
|
-
apiToken: {
|
|
3300
|
-
type: "string",
|
|
3301
|
-
description: "Your Jira API token",
|
|
3302
|
-
secret: true
|
|
3303
|
-
},
|
|
3304
|
-
projectKey: {
|
|
3305
|
-
type: "string",
|
|
3306
|
-
description: "Project key (e.g., PROJ)"
|
|
3307
|
-
}
|
|
3308
|
-
}
|
|
3309
|
-
},
|
|
3310
|
-
status: "coming-soon"
|
|
3311
|
-
},
|
|
3312
|
-
{
|
|
3313
|
-
id: "github",
|
|
3314
|
-
name: "\u{1F419} GitHub Issues",
|
|
3315
|
-
description: "Sync tasks with GitHub Issues",
|
|
3316
|
-
packageName: "@opentask/taskin-github-provider",
|
|
3317
|
-
configSchema: {
|
|
3318
|
-
required: ["owner", "repo", "token"],
|
|
3319
|
-
properties: {
|
|
3320
|
-
owner: {
|
|
3321
|
-
type: "string",
|
|
3322
|
-
description: "Repository owner (username or organization)"
|
|
3323
|
-
},
|
|
3324
|
-
repo: {
|
|
3325
|
-
type: "string",
|
|
3326
|
-
description: "Repository name"
|
|
3327
|
-
},
|
|
3328
|
-
token: {
|
|
3329
|
-
type: "string",
|
|
3330
|
-
description: "GitHub Personal Access Token",
|
|
3331
|
-
secret: true
|
|
3332
|
-
}
|
|
3333
|
-
}
|
|
3334
|
-
},
|
|
3335
|
-
status: "coming-soon"
|
|
3336
|
-
}
|
|
3337
|
-
];
|
|
3338
|
-
function getProviderById(id) {
|
|
3339
|
-
return AVAILABLE_PROVIDERS.find((p) => p.id === id);
|
|
3340
|
-
}
|
|
3341
|
-
function getAllProviders() {
|
|
3342
|
-
return AVAILABLE_PROVIDERS;
|
|
3343
|
-
}
|
|
3344
|
-
|
|
3345
|
-
// src/lib/provider-registry/provider-registry.types.ts
|
|
3346
|
-
init_esm_shims();
|
|
3347
|
-
|
|
3348
|
-
// src/lib/provider-factory/provider-factory.ts
|
|
3349
|
-
function expandProviderConfig(config) {
|
|
3350
|
-
const expanded = {};
|
|
3351
|
-
for (const [key, value] of Object.entries(config)) {
|
|
3352
|
-
expanded[key] = typeof value === "string" ? resolveEnvVars(value) : value;
|
|
3353
|
-
}
|
|
3354
|
-
return expanded;
|
|
3355
|
-
}
|
|
3356
|
-
var buildFileSystemProvider = async ({ projectRoot, providerConfig, tasksDirOverride }) => {
|
|
3357
|
-
const { FileSystemTaskProvider: FileSystemTaskProvider2, isMetadataStyleId: isMetadataStyleId2, UserRegistry: UserRegistry2 } = await Promise.resolve().then(() => (init_src2(), src_exports));
|
|
3358
|
-
const configuredTasksDir = typeof providerConfig.tasksDir === "string" ? providerConfig.tasksDir : "TASKS";
|
|
3359
|
-
const tasksDir = path7.resolve(projectRoot, tasksDirOverride ?? configuredTasksDir);
|
|
3360
|
-
const userRegistry = new UserRegistry2({ taskinDir: path7.join(projectRoot, ".taskin") });
|
|
3361
|
-
const metadataStyle = isMetadataStyleId2(providerConfig.metadataStyle) ? providerConfig.metadataStyle : void 0;
|
|
3362
|
-
const convertMetadataStyleTo = isMetadataStyleId2(providerConfig.convertMetadataStyleTo) ? providerConfig.convertMetadataStyleTo : void 0;
|
|
3363
|
-
const provider = new FileSystemTaskProvider2(tasksDir, userRegistry, void 0, void 0, {
|
|
3364
|
-
...metadataStyle !== void 0 && { metadataStyle },
|
|
3365
|
-
...convertMetadataStyleTo !== void 0 && { convertMetadataStyleTo }
|
|
3366
|
-
});
|
|
3367
|
-
return { provider, userRegistry };
|
|
3368
|
-
};
|
|
3369
|
-
var PROVIDER_BUILDERS = {
|
|
3370
|
-
fs: buildFileSystemProvider
|
|
3371
|
-
};
|
|
3372
|
-
function unknownProviderError(providerType, builders) {
|
|
3373
|
-
const known = getProviderById(providerType);
|
|
3374
|
-
if (known) {
|
|
3375
|
-
return new Error(
|
|
3376
|
-
`Provider "${providerType}" (${known.name}) is configured in .taskin.json but has no implementation yet (status: ${known.status}).
|
|
3377
|
-
Install/await ${known.packageName}, or change provider.type.`
|
|
3378
|
-
);
|
|
3379
|
-
}
|
|
3380
|
-
const buildable = Object.keys(builders).join(", ");
|
|
3381
|
-
const listed = getAllProviders().map((provider) => provider.id).join(", ");
|
|
3382
|
-
return new Error(
|
|
3383
|
-
`Unknown provider.type "${providerType}" in .taskin.json.
|
|
3384
|
-
Usable now: ${buildable}. Known ids: ${listed}.`
|
|
3385
|
-
);
|
|
3386
|
-
}
|
|
3387
|
-
async function resolveTaskProvider(options = {}, builders = PROVIDER_BUILDERS) {
|
|
3388
|
-
const projectRoot = path7.resolve(options.cwd ?? process.cwd());
|
|
3389
|
-
loadDotEnv(projectRoot);
|
|
3390
|
-
const config = new ConfigManager(projectRoot).loadConfig();
|
|
3391
|
-
const providerType = config.provider.type;
|
|
3392
|
-
const build = builders[providerType];
|
|
3393
|
-
if (!build) {
|
|
3394
|
-
throw unknownProviderError(providerType, builders);
|
|
3395
|
-
}
|
|
3396
|
-
const context = {
|
|
3397
|
-
projectRoot,
|
|
3398
|
-
providerConfig: { ...expandProviderConfig(config.provider.config), ...options.configOverrides },
|
|
3399
|
-
...options.tasksDir !== void 0 && { tasksDirOverride: options.tasksDir }
|
|
3400
|
-
};
|
|
3401
|
-
const { provider, userRegistry } = await build(context);
|
|
3402
|
-
await userRegistry.load();
|
|
3403
|
-
return { provider, userRegistry, projectRoot, providerType };
|
|
3404
|
-
}
|
|
3405
|
-
|
|
3406
|
-
// src/lib/provider-factory/provider-factory.types.ts
|
|
3407
|
-
init_esm_shims();
|
|
3408
|
-
|
|
3409
|
-
// src/commands/dashboard.ts
|
|
3410
|
-
var __filename2 = fileURLToPath2(import.meta.url);
|
|
3411
|
-
var __dirname2 = path8.dirname(__filename2);
|
|
3412
|
-
async function startHttpServer(app, startPort, host, maxAttempts = 10) {
|
|
3413
|
-
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
3414
|
-
const tryPort = startPort + attempt;
|
|
3415
|
-
try {
|
|
3416
|
-
const server = createServer(app);
|
|
3417
|
-
await new Promise((resolve, reject) => {
|
|
3418
|
-
server.once("error", reject);
|
|
3419
|
-
server.listen(tryPort, host, () => resolve());
|
|
3420
|
-
});
|
|
3421
|
-
return { server, port: tryPort };
|
|
3422
|
-
} catch (err) {
|
|
3423
|
-
const nodeErr = err;
|
|
3424
|
-
if (nodeErr.code !== "EADDRINUSE") {
|
|
3425
|
-
throw err;
|
|
3426
|
-
}
|
|
3427
|
-
if (attempt < maxAttempts - 1) {
|
|
3428
|
-
info(`Port ${tryPort} is in use, trying port ${tryPort + 1}...`);
|
|
3779
|
+
this.sendToClient(client.id, { type: "pong" });
|
|
3780
|
+
break;
|
|
3781
|
+
default:
|
|
3782
|
+
this.sendToClient(client.id, {
|
|
3783
|
+
type: "error",
|
|
3784
|
+
payload: { message: `Unknown message type: ${message.type}` }
|
|
3785
|
+
});
|
|
3429
3786
|
}
|
|
3787
|
+
} catch (error2) {
|
|
3788
|
+
this.log("Error handling message:", error2);
|
|
3789
|
+
this.sendToClient(client.id, {
|
|
3790
|
+
type: "error",
|
|
3791
|
+
payload: {
|
|
3792
|
+
message: error2 instanceof Error ? error2.message : "Failed to process message"
|
|
3793
|
+
}
|
|
3794
|
+
});
|
|
3430
3795
|
}
|
|
3431
3796
|
}
|
|
3432
|
-
|
|
3433
|
-
|
|
3434
|
-
|
|
3435
|
-
|
|
3436
|
-
|
|
3437
|
-
|
|
3438
|
-
|
|
3439
|
-
|
|
3440
|
-
|
|
3441
|
-
|
|
3442
|
-
|
|
3443
|
-
|
|
3444
|
-
|
|
3445
|
-
|
|
3446
|
-
|
|
3447
|
-
|
|
3448
|
-
|
|
3449
|
-
|
|
3450
|
-
},
|
|
3451
|
-
{
|
|
3452
|
-
flags: "-h, --host <host>",
|
|
3453
|
-
description: "Host to bind servers",
|
|
3454
|
-
defaultValue: "localhost"
|
|
3455
|
-
},
|
|
3456
|
-
{
|
|
3457
|
-
flags: "-b, --browser",
|
|
3458
|
-
description: "Open browser automatically"
|
|
3459
|
-
},
|
|
3460
|
-
{
|
|
3461
|
-
flags: "--open",
|
|
3462
|
-
description: "Show only open tasks (pending, in-progress, blocked)"
|
|
3463
|
-
},
|
|
3464
|
-
{
|
|
3465
|
-
flags: "--closed",
|
|
3466
|
-
description: "Show only closed tasks (done, canceled)"
|
|
3797
|
+
/**
|
|
3798
|
+
* Reads the task id out of an incoming message.
|
|
3799
|
+
*
|
|
3800
|
+
* Ids on the wire are untrusted strings: a branded `TaskId` is only worth
|
|
3801
|
+
* something if the boundary that produces it actually validates. Answers the
|
|
3802
|
+
* client with a clear error instead of letting a ZodError leak out of the
|
|
3803
|
+
* generic catch.
|
|
3804
|
+
*/
|
|
3805
|
+
readTaskId(client, message, field = "taskId") {
|
|
3806
|
+
const raw = message.payload?.[field];
|
|
3807
|
+
const parsed = typeof raw === "string" ? TaskIdSchema.safeParse(raw) : void 0;
|
|
3808
|
+
if (!parsed?.success) {
|
|
3809
|
+
this.sendToClient(client.id, {
|
|
3810
|
+
type: "error",
|
|
3811
|
+
payload: { message: `Invalid task id in '${message.type}' request` },
|
|
3812
|
+
requestId: message.requestId
|
|
3813
|
+
});
|
|
3814
|
+
return void 0;
|
|
3467
3815
|
}
|
|
3468
|
-
|
|
3469
|
-
handler: async (options) => {
|
|
3470
|
-
await startDashboard(options);
|
|
3471
|
-
}
|
|
3472
|
-
});
|
|
3473
|
-
async function startDashboard(options) {
|
|
3474
|
-
requireTaskinProject();
|
|
3475
|
-
const host = options.host || "localhost";
|
|
3476
|
-
if (!isValidHost(host)) {
|
|
3477
|
-
error("Security validation failed");
|
|
3478
|
-
error(`Invalid host: ${host}. Must be localhost, a valid IPv4 address, or hostname.`);
|
|
3479
|
-
process.exit(1);
|
|
3480
|
-
}
|
|
3481
|
-
if (typeof options.port === "string" && !isValidPort(options.port)) {
|
|
3482
|
-
error("Security validation failed");
|
|
3483
|
-
error(`Invalid port: ${options.port}. Must be between 1 and 65535.`);
|
|
3484
|
-
process.exit(1);
|
|
3485
|
-
}
|
|
3486
|
-
if (typeof options.wsPort === "string" && !isValidPort(options.wsPort)) {
|
|
3487
|
-
error("Security validation failed");
|
|
3488
|
-
error(`Invalid WebSocket port: ${options.wsPort}. Must be between 1 and 65535.`);
|
|
3489
|
-
process.exit(1);
|
|
3816
|
+
return parsed.data;
|
|
3490
3817
|
}
|
|
3491
|
-
|
|
3492
|
-
|
|
3493
|
-
|
|
3494
|
-
|
|
3495
|
-
|
|
3496
|
-
|
|
3818
|
+
/**
|
|
3819
|
+
* Handle list request
|
|
3820
|
+
*/
|
|
3821
|
+
async handleListRequest(client, message) {
|
|
3822
|
+
const tasks = await this.taskProvider.getAllTasks();
|
|
3823
|
+
const [first] = tasks;
|
|
3824
|
+
if (first) {
|
|
3825
|
+
this.log("[WS Server] Sending", tasks.length, "tasks");
|
|
3826
|
+
this.log("[WS Server] First task assignee:", first.assignee);
|
|
3827
|
+
}
|
|
3828
|
+
this.sendToClient(client.id, {
|
|
3829
|
+
type: "tasks",
|
|
3830
|
+
payload: tasks,
|
|
3831
|
+
requestId: message.requestId
|
|
3832
|
+
});
|
|
3497
3833
|
}
|
|
3498
|
-
|
|
3499
|
-
|
|
3500
|
-
|
|
3501
|
-
|
|
3834
|
+
/**
|
|
3835
|
+
* Handle find request
|
|
3836
|
+
*/
|
|
3837
|
+
async handleFindRequest(client, message) {
|
|
3838
|
+
const taskId = this.readTaskId(client, message);
|
|
3839
|
+
if (!taskId)
|
|
3840
|
+
return;
|
|
3841
|
+
const task = await this.taskProvider.findTask(taskId);
|
|
3842
|
+
this.sendToClient(client.id, {
|
|
3843
|
+
type: "task:found",
|
|
3844
|
+
payload: task,
|
|
3845
|
+
requestId: message.requestId
|
|
3846
|
+
});
|
|
3502
3847
|
}
|
|
3503
|
-
|
|
3504
|
-
|
|
3505
|
-
|
|
3506
|
-
|
|
3507
|
-
|
|
3508
|
-
|
|
3509
|
-
|
|
3510
|
-
|
|
3511
|
-
|
|
3512
|
-
|
|
3513
|
-
|
|
3514
|
-
|
|
3515
|
-
|
|
3516
|
-
|
|
3517
|
-
|
|
3518
|
-
currentDir = path8.dirname(currentDir);
|
|
3519
|
-
}
|
|
3520
|
-
}
|
|
3848
|
+
/**
|
|
3849
|
+
* Handle update request
|
|
3850
|
+
*/
|
|
3851
|
+
async handleUpdateRequest(client, message) {
|
|
3852
|
+
const taskId = this.readTaskId(client, message, "id");
|
|
3853
|
+
if (!taskId)
|
|
3854
|
+
return;
|
|
3855
|
+
const stored = await this.taskProvider.findTask(taskId);
|
|
3856
|
+
if (!stored) {
|
|
3857
|
+
this.sendToClient(client.id, {
|
|
3858
|
+
type: "error",
|
|
3859
|
+
payload: { message: `Task ${taskId} not found` },
|
|
3860
|
+
requestId: message.requestId
|
|
3861
|
+
});
|
|
3862
|
+
return;
|
|
3521
3863
|
}
|
|
3522
|
-
|
|
3523
|
-
|
|
3524
|
-
|
|
3525
|
-
|
|
3526
|
-
|
|
3527
|
-
|
|
3528
|
-
|
|
3529
|
-
|
|
3530
|
-
|
|
3531
|
-
|
|
3532
|
-
|
|
3533
|
-
|
|
3864
|
+
const outcome = applyTaskUpdate(stored, message.payload);
|
|
3865
|
+
if (!outcome.ok) {
|
|
3866
|
+
this.sendToClient(client.id, {
|
|
3867
|
+
type: "error",
|
|
3868
|
+
payload: { message: outcome.message },
|
|
3869
|
+
requestId: message.requestId
|
|
3870
|
+
});
|
|
3871
|
+
return;
|
|
3872
|
+
}
|
|
3873
|
+
await this.taskProvider.updateTask(outcome.task);
|
|
3874
|
+
this.broadcast({
|
|
3875
|
+
type: "task:updated",
|
|
3876
|
+
payload: outcome.task
|
|
3534
3877
|
});
|
|
3535
|
-
|
|
3536
|
-
|
|
3537
|
-
|
|
3538
|
-
|
|
3539
|
-
|
|
3540
|
-
const
|
|
3541
|
-
|
|
3542
|
-
|
|
3543
|
-
|
|
3544
|
-
|
|
3545
|
-
|
|
3546
|
-
|
|
3547
|
-
"Content-Security-Policy",
|
|
3548
|
-
"default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; connect-src 'self' ws: wss:;"
|
|
3549
|
-
);
|
|
3550
|
-
next();
|
|
3878
|
+
}
|
|
3879
|
+
/**
|
|
3880
|
+
* Handle start task request
|
|
3881
|
+
*/
|
|
3882
|
+
async handleStartRequest(client, message) {
|
|
3883
|
+
const taskId = this.readTaskId(client, message);
|
|
3884
|
+
if (!taskId)
|
|
3885
|
+
return;
|
|
3886
|
+
const task = await this.taskManager.startTask(taskId);
|
|
3887
|
+
this.broadcast({
|
|
3888
|
+
type: "task:updated",
|
|
3889
|
+
payload: task
|
|
3551
3890
|
});
|
|
3552
|
-
|
|
3553
|
-
|
|
3554
|
-
|
|
3555
|
-
|
|
3556
|
-
|
|
3557
|
-
|
|
3558
|
-
|
|
3559
|
-
|
|
3560
|
-
|
|
3561
|
-
|
|
3562
|
-
|
|
3563
|
-
|
|
3564
|
-
res.status(500).send("Internal Server Error");
|
|
3565
|
-
});
|
|
3566
|
-
} else {
|
|
3567
|
-
next();
|
|
3568
|
-
}
|
|
3891
|
+
}
|
|
3892
|
+
/**
|
|
3893
|
+
* Handle finish task request
|
|
3894
|
+
*/
|
|
3895
|
+
async handleFinishRequest(client, message) {
|
|
3896
|
+
const taskId = this.readTaskId(client, message);
|
|
3897
|
+
if (!taskId)
|
|
3898
|
+
return;
|
|
3899
|
+
const task = await this.taskManager.finishTask(taskId);
|
|
3900
|
+
this.broadcast({
|
|
3901
|
+
type: "task:updated",
|
|
3902
|
+
payload: task
|
|
3569
3903
|
});
|
|
3570
|
-
|
|
3571
|
-
|
|
3572
|
-
|
|
3573
|
-
|
|
3574
|
-
|
|
3575
|
-
|
|
3576
|
-
|
|
3577
|
-
|
|
3578
|
-
|
|
3579
|
-
|
|
3580
|
-
|
|
3581
|
-
|
|
3904
|
+
}
|
|
3905
|
+
/**
|
|
3906
|
+
* Handle pause task request
|
|
3907
|
+
*/
|
|
3908
|
+
async handlePauseRequest(client, message) {
|
|
3909
|
+
const taskId = this.readTaskId(client, message);
|
|
3910
|
+
if (!taskId)
|
|
3911
|
+
return;
|
|
3912
|
+
const task = await this.taskManager.pauseTask(taskId);
|
|
3913
|
+
this.broadcast({
|
|
3914
|
+
type: "task:updated",
|
|
3915
|
+
payload: task
|
|
3582
3916
|
});
|
|
3583
|
-
|
|
3584
|
-
|
|
3585
|
-
|
|
3586
|
-
|
|
3587
|
-
|
|
3588
|
-
|
|
3589
|
-
|
|
3590
|
-
|
|
3591
|
-
|
|
3592
|
-
|
|
3593
|
-
|
|
3594
|
-
|
|
3595
|
-
|
|
3596
|
-
|
|
3597
|
-
|
|
3598
|
-
const cmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
|
|
3599
|
-
cp.exec(`${cmd} ${url}`);
|
|
3917
|
+
}
|
|
3918
|
+
/**
|
|
3919
|
+
* Start heartbeat to check client connections
|
|
3920
|
+
*/
|
|
3921
|
+
startHeartbeat() {
|
|
3922
|
+
this.heartbeatInterval = setInterval(() => {
|
|
3923
|
+
this.clients.forEach((client) => {
|
|
3924
|
+
if (!client.isAlive) {
|
|
3925
|
+
this.log(`Client ${client.id} timeout, terminating`);
|
|
3926
|
+
client.ws.terminate();
|
|
3927
|
+
this.clients.delete(client.id);
|
|
3928
|
+
return;
|
|
3929
|
+
}
|
|
3930
|
+
client.isAlive = false;
|
|
3931
|
+
client.ws.ping();
|
|
3600
3932
|
});
|
|
3933
|
+
}, this.options.heartbeatInterval);
|
|
3934
|
+
}
|
|
3935
|
+
/**
|
|
3936
|
+
* Stop heartbeat
|
|
3937
|
+
*/
|
|
3938
|
+
stopHeartbeat() {
|
|
3939
|
+
if (this.heartbeatInterval) {
|
|
3940
|
+
clearInterval(this.heartbeatInterval);
|
|
3941
|
+
this.heartbeatInterval = null;
|
|
3601
3942
|
}
|
|
3602
|
-
|
|
3603
|
-
|
|
3604
|
-
|
|
3605
|
-
|
|
3606
|
-
|
|
3607
|
-
|
|
3608
|
-
|
|
3609
|
-
info(` \u2022 Filter: ${chalk4.yellow("Closed tasks only")}`);
|
|
3610
|
-
}
|
|
3611
|
-
info(` \u2022 Press ${chalk4.bold("Ctrl+C")} to stop both servers`);
|
|
3612
|
-
info("");
|
|
3613
|
-
const cleanup = async () => {
|
|
3614
|
-
info("\nShutting down servers...");
|
|
3615
|
-
await new Promise((resolve) => {
|
|
3616
|
-
httpServer.close(() => resolve());
|
|
3617
|
-
});
|
|
3618
|
-
await wsServer.stop();
|
|
3619
|
-
success("\u2713 Servers stopped");
|
|
3620
|
-
process.exit(0);
|
|
3621
|
-
};
|
|
3622
|
-
process.on("SIGINT", cleanup);
|
|
3623
|
-
process.on("SIGTERM", cleanup);
|
|
3624
|
-
} catch (err) {
|
|
3625
|
-
error("Failed to start dashboard");
|
|
3626
|
-
if (err instanceof Error) {
|
|
3627
|
-
error(err.message);
|
|
3943
|
+
}
|
|
3944
|
+
/**
|
|
3945
|
+
* Debug logging
|
|
3946
|
+
*/
|
|
3947
|
+
log(...args) {
|
|
3948
|
+
if (this.options.debug) {
|
|
3949
|
+
console.log("[TaskWebSocketServer]", ...args);
|
|
3628
3950
|
}
|
|
3629
|
-
process.exit(1);
|
|
3630
3951
|
}
|
|
3631
|
-
}
|
|
3952
|
+
};
|
|
3632
3953
|
|
|
3633
|
-
//
|
|
3954
|
+
// node_modules/@opentask/taskin-task-server-ws/dist/task-server-ws.mock.js
|
|
3955
|
+
init_esm_shims();
|
|
3956
|
+
import { parseTaskId } from "@opentask/taskin-types";
|
|
3957
|
+
|
|
3958
|
+
// node_modules/@opentask/taskin-task-server-ws/dist/task-server-ws.types.js
|
|
3634
3959
|
init_esm_shims();
|
|
3960
|
+
|
|
3961
|
+
// src/commands/dashboard.ts
|
|
3635
3962
|
init_src2();
|
|
3963
|
+
import chalk4 from "chalk";
|
|
3964
|
+
import express from "express";
|
|
3965
|
+
import { createServer } from "http";
|
|
3966
|
+
import path8 from "path";
|
|
3967
|
+
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
3636
3968
|
|
|
3637
|
-
//
|
|
3969
|
+
// src/lib/provider-factory/index.ts
|
|
3638
3970
|
init_esm_shims();
|
|
3639
3971
|
|
|
3640
|
-
//
|
|
3972
|
+
// src/lib/provider-factory/provider-factory.ts
|
|
3641
3973
|
init_esm_shims();
|
|
3642
|
-
import
|
|
3643
|
-
|
|
3974
|
+
import path7 from "path";
|
|
3975
|
+
|
|
3976
|
+
// src/lib/notification/env-resolver.ts
|
|
3977
|
+
init_esm_shims();
|
|
3978
|
+
import { readFileSync as readFileSync2 } from "fs";
|
|
3979
|
+
import { join as join2 } from "path";
|
|
3980
|
+
var ENV_VAR_PATTERN = /\$\{([^}]+)\}/g;
|
|
3981
|
+
function loadDotEnv(dir) {
|
|
3644
3982
|
try {
|
|
3645
|
-
|
|
3646
|
-
|
|
3647
|
-
|
|
3648
|
-
|
|
3983
|
+
const envPath = join2(dir ?? process.cwd(), ".env");
|
|
3984
|
+
const content = readFileSync2(envPath, "utf-8");
|
|
3985
|
+
for (const line of content.split("\n")) {
|
|
3986
|
+
const trimmed = line.trim();
|
|
3987
|
+
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
3988
|
+
const eqIndex = trimmed.indexOf("=");
|
|
3989
|
+
if (eqIndex === -1) continue;
|
|
3990
|
+
const key = trimmed.slice(0, eqIndex).trim();
|
|
3991
|
+
let value = trimmed.slice(eqIndex + 1).trim();
|
|
3992
|
+
if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) {
|
|
3993
|
+
value = value.slice(1, -1);
|
|
3994
|
+
}
|
|
3995
|
+
if (!process.env[key]) {
|
|
3996
|
+
process.env[key] = value;
|
|
3997
|
+
}
|
|
3998
|
+
}
|
|
3649
3999
|
} catch {
|
|
3650
|
-
return "";
|
|
3651
4000
|
}
|
|
3652
4001
|
}
|
|
3653
|
-
function
|
|
3654
|
-
return
|
|
3655
|
-
|
|
3656
|
-
|
|
3657
|
-
return executeGit("branch --show-current");
|
|
4002
|
+
function resolveEnvVars(value) {
|
|
4003
|
+
return value.replace(ENV_VAR_PATTERN, (_match, varName) => {
|
|
4004
|
+
return process.env[varName] ?? "";
|
|
4005
|
+
});
|
|
3658
4006
|
}
|
|
3659
|
-
|
|
3660
|
-
|
|
3661
|
-
|
|
4007
|
+
|
|
4008
|
+
// src/lib/provider-registry/index.ts
|
|
4009
|
+
init_esm_shims();
|
|
4010
|
+
|
|
4011
|
+
// src/lib/provider-registry/provider-registry.ts
|
|
4012
|
+
init_esm_shims();
|
|
4013
|
+
var AVAILABLE_PROVIDERS = [
|
|
4014
|
+
{
|
|
4015
|
+
id: "fs",
|
|
4016
|
+
name: "\u{1F4C1} File System",
|
|
4017
|
+
description: "Store tasks as Markdown files in a local TASKS/ directory",
|
|
4018
|
+
packageName: "@opentask/taskin-file-system-provider",
|
|
4019
|
+
configSchema: {
|
|
4020
|
+
required: ["tasksDir"],
|
|
4021
|
+
properties: {
|
|
4022
|
+
tasksDir: {
|
|
4023
|
+
type: "string",
|
|
4024
|
+
description: "Directory to store task files"
|
|
4025
|
+
},
|
|
4026
|
+
metadataStyle: {
|
|
4027
|
+
type: "string",
|
|
4028
|
+
description: "Marking of the metadata block for new files (list | hard-break | plain)"
|
|
4029
|
+
}
|
|
4030
|
+
}
|
|
4031
|
+
},
|
|
4032
|
+
status: "stable"
|
|
4033
|
+
},
|
|
4034
|
+
{
|
|
4035
|
+
id: "redmine",
|
|
4036
|
+
name: "\u{1F534} Redmine",
|
|
4037
|
+
description: "Sync tasks with Redmine issues via REST API",
|
|
4038
|
+
packageName: "@opentask/taskin-redmine-provider",
|
|
4039
|
+
configSchema: {
|
|
4040
|
+
required: ["apiUrl", "apiKey", "projectId"],
|
|
4041
|
+
properties: {
|
|
4042
|
+
apiUrl: {
|
|
4043
|
+
type: "string",
|
|
4044
|
+
description: "Redmine server URL (e.g., https://redmine.example.com)"
|
|
4045
|
+
},
|
|
4046
|
+
apiKey: {
|
|
4047
|
+
type: "string",
|
|
4048
|
+
description: "Your Redmine API key",
|
|
4049
|
+
secret: true
|
|
4050
|
+
},
|
|
4051
|
+
projectId: {
|
|
4052
|
+
type: "string",
|
|
4053
|
+
description: "Project identifier or ID"
|
|
4054
|
+
}
|
|
4055
|
+
}
|
|
4056
|
+
},
|
|
4057
|
+
status: "coming-soon"
|
|
4058
|
+
},
|
|
4059
|
+
{
|
|
4060
|
+
id: "jira",
|
|
4061
|
+
name: "\u{1F535} Jira",
|
|
4062
|
+
description: "Sync tasks with Jira issues via REST API",
|
|
4063
|
+
packageName: "@opentask/taskin-jira-provider",
|
|
4064
|
+
configSchema: {
|
|
4065
|
+
required: ["apiUrl", "email", "apiToken", "projectKey"],
|
|
4066
|
+
properties: {
|
|
4067
|
+
apiUrl: {
|
|
4068
|
+
type: "string",
|
|
4069
|
+
description: "Jira server URL (e.g., https://company.atlassian.net)"
|
|
4070
|
+
},
|
|
4071
|
+
email: {
|
|
4072
|
+
type: "string",
|
|
4073
|
+
description: "Your Atlassian account email"
|
|
4074
|
+
},
|
|
4075
|
+
apiToken: {
|
|
4076
|
+
type: "string",
|
|
4077
|
+
description: "Your Jira API token",
|
|
4078
|
+
secret: true
|
|
4079
|
+
},
|
|
4080
|
+
projectKey: {
|
|
4081
|
+
type: "string",
|
|
4082
|
+
description: "Project key (e.g., PROJ)"
|
|
4083
|
+
}
|
|
4084
|
+
}
|
|
4085
|
+
},
|
|
4086
|
+
status: "coming-soon"
|
|
4087
|
+
},
|
|
4088
|
+
{
|
|
4089
|
+
id: "github",
|
|
4090
|
+
name: "\u{1F419} GitHub Issues",
|
|
4091
|
+
description: "Sync tasks with GitHub Issues",
|
|
4092
|
+
packageName: "@opentask/taskin-github-provider",
|
|
4093
|
+
configSchema: {
|
|
4094
|
+
required: ["owner", "repo", "token"],
|
|
4095
|
+
properties: {
|
|
4096
|
+
owner: {
|
|
4097
|
+
type: "string",
|
|
4098
|
+
description: "Repository owner (username or organization)"
|
|
4099
|
+
},
|
|
4100
|
+
repo: {
|
|
4101
|
+
type: "string",
|
|
4102
|
+
description: "Repository name"
|
|
4103
|
+
},
|
|
4104
|
+
token: {
|
|
4105
|
+
type: "string",
|
|
4106
|
+
description: "GitHub Personal Access Token",
|
|
4107
|
+
secret: true
|
|
4108
|
+
}
|
|
4109
|
+
}
|
|
4110
|
+
},
|
|
4111
|
+
status: "coming-soon"
|
|
4112
|
+
}
|
|
4113
|
+
];
|
|
4114
|
+
function getProviderById(id) {
|
|
4115
|
+
return AVAILABLE_PROVIDERS.find((p) => p.id === id);
|
|
4116
|
+
}
|
|
4117
|
+
function getAllProviders() {
|
|
4118
|
+
return AVAILABLE_PROVIDERS;
|
|
3662
4119
|
}
|
|
3663
4120
|
|
|
3664
|
-
//
|
|
4121
|
+
// src/lib/provider-registry/provider-registry.types.ts
|
|
3665
4122
|
init_esm_shims();
|
|
3666
4123
|
|
|
3667
|
-
//
|
|
3668
|
-
|
|
3669
|
-
|
|
3670
|
-
|
|
3671
|
-
|
|
3672
|
-
function isNodeError(error2) {
|
|
3673
|
-
return typeof error2 === "object" && error2 !== null && "code" in error2;
|
|
3674
|
-
}
|
|
3675
|
-
async function executeGit2(command, cwd) {
|
|
3676
|
-
try {
|
|
3677
|
-
const { stdout } = await execAsync(`git ${command}`, {
|
|
3678
|
-
cwd: cwd || process.cwd(),
|
|
3679
|
-
encoding: "utf8",
|
|
3680
|
-
maxBuffer: 10 * 1024 * 1024,
|
|
3681
|
-
// 10MB buffer for large repos
|
|
3682
|
-
timeout: 3e4
|
|
3683
|
-
// 30 second timeout
|
|
3684
|
-
});
|
|
3685
|
-
return stdout.trim();
|
|
3686
|
-
} catch (error2) {
|
|
3687
|
-
if (isNodeError(error2)) {
|
|
3688
|
-
if (error2.code === "ENOENT") {
|
|
3689
|
-
throw new Error("Git is not installed or not in PATH");
|
|
3690
|
-
}
|
|
3691
|
-
}
|
|
3692
|
-
return "";
|
|
4124
|
+
// src/lib/provider-factory/provider-factory.ts
|
|
4125
|
+
function expandProviderConfig(config) {
|
|
4126
|
+
const expanded = {};
|
|
4127
|
+
for (const [key, value] of Object.entries(config)) {
|
|
4128
|
+
expanded[key] = typeof value === "string" ? resolveEnvVars(value) : value;
|
|
3693
4129
|
}
|
|
4130
|
+
return expanded;
|
|
3694
4131
|
}
|
|
3695
|
-
var
|
|
3696
|
-
|
|
3697
|
-
|
|
3698
|
-
|
|
3699
|
-
|
|
3700
|
-
|
|
3701
|
-
|
|
3702
|
-
|
|
3703
|
-
|
|
3704
|
-
|
|
3705
|
-
|
|
3706
|
-
}
|
|
3707
|
-
|
|
3708
|
-
|
|
3709
|
-
|
|
3710
|
-
|
|
3711
|
-
|
|
3712
|
-
|
|
3713
|
-
|
|
3714
|
-
|
|
3715
|
-
|
|
3716
|
-
|
|
3717
|
-
"--pretty='format:%H|%an|%aI|%s%x00%b'",
|
|
3718
|
-
"--numstat"
|
|
3719
|
-
// Get file stats
|
|
4132
|
+
var buildFileSystemProvider = async ({ projectRoot, providerConfig, tasksDirOverride }) => {
|
|
4133
|
+
const { FileSystemTaskProvider: FileSystemTaskProvider2, isMetadataStyleId: isMetadataStyleId2, UserRegistry: UserRegistry2 } = await Promise.resolve().then(() => (init_src3(), src_exports));
|
|
4134
|
+
const configuredTasksDir = typeof providerConfig.tasksDir === "string" ? providerConfig.tasksDir : "TASKS";
|
|
4135
|
+
const tasksDir = path7.resolve(projectRoot, tasksDirOverride ?? configuredTasksDir);
|
|
4136
|
+
const userRegistry = new UserRegistry2({ taskinDir: path7.join(projectRoot, ".taskin") });
|
|
4137
|
+
const metadataStyle = isMetadataStyleId2(providerConfig.metadataStyle) ? providerConfig.metadataStyle : void 0;
|
|
4138
|
+
const convertMetadataStyleTo = isMetadataStyleId2(providerConfig.convertMetadataStyleTo) ? providerConfig.convertMetadataStyleTo : void 0;
|
|
4139
|
+
const provider = new FileSystemTaskProvider2(tasksDir, userRegistry, void 0, void 0, {
|
|
4140
|
+
...metadataStyle !== void 0 && { metadataStyle },
|
|
4141
|
+
...convertMetadataStyleTo !== void 0 && { convertMetadataStyleTo }
|
|
4142
|
+
});
|
|
4143
|
+
return { provider, userRegistry };
|
|
4144
|
+
};
|
|
4145
|
+
var PROVIDER_BUILDERS = {
|
|
4146
|
+
fs: buildFileSystemProvider
|
|
4147
|
+
};
|
|
4148
|
+
function unknownProviderError(providerType, builders) {
|
|
4149
|
+
const known = getProviderById(providerType);
|
|
4150
|
+
if (known) {
|
|
4151
|
+
return new Error(
|
|
4152
|
+
`Provider "${providerType}" (${known.name}) is configured in .taskin.json but has no implementation yet (status: ${known.status}).
|
|
4153
|
+
Install/await ${known.packageName}, or change provider.type.`
|
|
3720
4154
|
);
|
|
3721
|
-
if (options.since) {
|
|
3722
|
-
args.push(`--since="${options.since}"`);
|
|
3723
|
-
}
|
|
3724
|
-
if (options.until) {
|
|
3725
|
-
args.push(`--until=${options.until}`);
|
|
3726
|
-
}
|
|
3727
|
-
if (options.author) {
|
|
3728
|
-
args.push(`--author='${options.author}'`);
|
|
3729
|
-
}
|
|
3730
|
-
if (options.maxCount) {
|
|
3731
|
-
args.push(`-n ${options.maxCount}`);
|
|
3732
|
-
}
|
|
3733
|
-
if (!options.includeMerges) {
|
|
3734
|
-
args.push("--no-merges");
|
|
3735
|
-
}
|
|
3736
|
-
if (options.filePath) {
|
|
3737
|
-
args.push("--", options.filePath);
|
|
3738
|
-
}
|
|
3739
|
-
const command = args.join(" ");
|
|
3740
|
-
const output = await executeGit2(command, this.repositoryPath);
|
|
3741
|
-
if (!output) {
|
|
3742
|
-
return [];
|
|
3743
|
-
}
|
|
3744
|
-
return this.parseCommits(output);
|
|
3745
|
-
}
|
|
3746
|
-
parseCommits(output) {
|
|
3747
|
-
const commits = [];
|
|
3748
|
-
const lines = output.split("\n");
|
|
3749
|
-
let i = 0;
|
|
3750
|
-
while (i < lines.length) {
|
|
3751
|
-
const line = lines[i];
|
|
3752
|
-
if (line === void 0) break;
|
|
3753
|
-
if (!line.trim()) {
|
|
3754
|
-
i++;
|
|
3755
|
-
continue;
|
|
3756
|
-
}
|
|
3757
|
-
if (line.includes("|")) {
|
|
3758
|
-
const [hash, author, date, messageWithBody] = line.split("|");
|
|
3759
|
-
if (hash === void 0 || author === void 0 || date === void 0 || messageWithBody === void 0) {
|
|
3760
|
-
i++;
|
|
3761
|
-
continue;
|
|
3762
|
-
}
|
|
3763
|
-
if (!/^[0-9a-f]{6,40}$/i.test(hash)) {
|
|
3764
|
-
i++;
|
|
3765
|
-
continue;
|
|
3766
|
-
}
|
|
3767
|
-
const nullByteIndex = messageWithBody.indexOf("\0");
|
|
3768
|
-
const subject = nullByteIndex !== -1 ? messageWithBody.substring(0, nullByteIndex) : messageWithBody;
|
|
3769
|
-
const bodyLines = [];
|
|
3770
|
-
if (nullByteIndex !== -1) {
|
|
3771
|
-
bodyLines.push(messageWithBody.substring(nullByteIndex + 1));
|
|
3772
|
-
}
|
|
3773
|
-
i++;
|
|
3774
|
-
while (i < lines.length) {
|
|
3775
|
-
const bodyLine = lines[i];
|
|
3776
|
-
if (bodyLine === void 0 || bodyLine.includes("|") || bodyLine.includes(" ")) break;
|
|
3777
|
-
if (bodyLine.trim()) {
|
|
3778
|
-
bodyLines.push(bodyLine);
|
|
3779
|
-
}
|
|
3780
|
-
i++;
|
|
3781
|
-
}
|
|
3782
|
-
const body = bodyLines.join("\n");
|
|
3783
|
-
const fullMessage = body ? `${subject}
|
|
3784
|
-
|
|
3785
|
-
${body}` : subject;
|
|
3786
|
-
let filesChanged = 0;
|
|
3787
|
-
let linesAdded = 0;
|
|
3788
|
-
let linesRemoved = 0;
|
|
3789
|
-
while (i < lines.length) {
|
|
3790
|
-
const rawStatLine = lines[i];
|
|
3791
|
-
if (rawStatLine === void 0 || rawStatLine.includes("|")) break;
|
|
3792
|
-
const statLine = rawStatLine.trim();
|
|
3793
|
-
if (!statLine) {
|
|
3794
|
-
i++;
|
|
3795
|
-
continue;
|
|
3796
|
-
}
|
|
3797
|
-
const [added, removed] = statLine.split(" ");
|
|
3798
|
-
if (added !== "-" && removed !== "-") {
|
|
3799
|
-
linesAdded += parseInt(added || "0", 10);
|
|
3800
|
-
linesRemoved += parseInt(removed || "0", 10);
|
|
3801
|
-
filesChanged++;
|
|
3802
|
-
}
|
|
3803
|
-
i++;
|
|
3804
|
-
}
|
|
3805
|
-
commits.push({
|
|
3806
|
-
hash,
|
|
3807
|
-
author,
|
|
3808
|
-
date,
|
|
3809
|
-
message: subject,
|
|
3810
|
-
filesChanged,
|
|
3811
|
-
linesAdded,
|
|
3812
|
-
linesRemoved,
|
|
3813
|
-
coAuthors: this.extractCoAuthors(fullMessage)
|
|
3814
|
-
});
|
|
3815
|
-
} else {
|
|
3816
|
-
i++;
|
|
3817
|
-
}
|
|
3818
|
-
}
|
|
3819
|
-
return commits;
|
|
3820
|
-
}
|
|
3821
|
-
extractCoAuthors(message) {
|
|
3822
|
-
const coAuthorRegex = /Co-authored-by:\s*(.+?)\s*<(.+?)>/gi;
|
|
3823
|
-
const matches = Array.from(message.matchAll(coAuthorRegex));
|
|
3824
|
-
const names = matches.flatMap((match) => match[1]?.trim() ?? []);
|
|
3825
|
-
return names.length > 0 ? names : void 0;
|
|
3826
|
-
}
|
|
3827
|
-
async getDiff(from = "HEAD", to = "") {
|
|
3828
|
-
const args = ["diff", "--numstat"];
|
|
3829
|
-
if (to) {
|
|
3830
|
-
args.push(`${from}..${to}`);
|
|
3831
|
-
} else {
|
|
3832
|
-
args.push(from);
|
|
3833
|
-
}
|
|
3834
|
-
const output = await executeGit2(args.join(" "), this.repositoryPath);
|
|
3835
|
-
return this.parseDiff(output);
|
|
3836
|
-
}
|
|
3837
|
-
parseDiff(output) {
|
|
3838
|
-
const files = [];
|
|
3839
|
-
let totalLinesAdded = 0;
|
|
3840
|
-
let totalLinesRemoved = 0;
|
|
3841
|
-
if (!output) {
|
|
3842
|
-
return {
|
|
3843
|
-
files,
|
|
3844
|
-
totalLinesAdded,
|
|
3845
|
-
totalLinesRemoved,
|
|
3846
|
-
netChange: 0
|
|
3847
|
-
};
|
|
3848
|
-
}
|
|
3849
|
-
const lines = output.split("\n").filter((l) => l.trim());
|
|
3850
|
-
for (const line of lines) {
|
|
3851
|
-
const [added, removed, path12] = line.split(" ");
|
|
3852
|
-
if (added === void 0 || removed === void 0 || path12 === void 0) continue;
|
|
3853
|
-
if (added === "-" || removed === "-") {
|
|
3854
|
-
continue;
|
|
3855
|
-
}
|
|
3856
|
-
const linesAdded = parseInt(added, 10);
|
|
3857
|
-
const linesRemoved = parseInt(removed, 10);
|
|
3858
|
-
totalLinesAdded += linesAdded;
|
|
3859
|
-
totalLinesRemoved += linesRemoved;
|
|
3860
|
-
files.push({
|
|
3861
|
-
path: path12,
|
|
3862
|
-
linesAdded,
|
|
3863
|
-
linesRemoved,
|
|
3864
|
-
changeType: "modified"
|
|
3865
|
-
// Simplified - could be enhanced with --name-status
|
|
3866
|
-
});
|
|
3867
|
-
}
|
|
3868
|
-
return {
|
|
3869
|
-
files,
|
|
3870
|
-
totalLinesAdded,
|
|
3871
|
-
totalLinesRemoved,
|
|
3872
|
-
netChange: totalLinesAdded - totalLinesRemoved
|
|
3873
|
-
};
|
|
3874
|
-
}
|
|
3875
|
-
async getFileDiff(filePath, from = "HEAD", to = "") {
|
|
3876
|
-
const args = ["diff", "--numstat"];
|
|
3877
|
-
if (to) {
|
|
3878
|
-
args.push(`${from}..${to}`);
|
|
3879
|
-
} else {
|
|
3880
|
-
args.push(from);
|
|
3881
|
-
}
|
|
3882
|
-
args.push("--", filePath);
|
|
3883
|
-
const output = await executeGit2(args.join(" "), this.repositoryPath);
|
|
3884
|
-
if (!output) {
|
|
3885
|
-
return null;
|
|
3886
|
-
}
|
|
3887
|
-
const [added, removed, path12] = output.split(" ");
|
|
3888
|
-
if (added === void 0 || removed === void 0 || path12 === void 0) {
|
|
3889
|
-
return null;
|
|
3890
|
-
}
|
|
3891
|
-
if (added === "-" || removed === "-") {
|
|
3892
|
-
return null;
|
|
3893
|
-
}
|
|
3894
|
-
return {
|
|
3895
|
-
path: path12,
|
|
3896
|
-
linesAdded: parseInt(added, 10),
|
|
3897
|
-
linesRemoved: parseInt(removed, 10),
|
|
3898
|
-
changeType: "modified"
|
|
3899
|
-
};
|
|
3900
|
-
}
|
|
3901
|
-
async getBlame(filePath) {
|
|
3902
|
-
const args = ["blame", "--line-porcelain", filePath];
|
|
3903
|
-
const output = await executeGit2(args.join(" "), this.repositoryPath);
|
|
3904
|
-
if (!output) {
|
|
3905
|
-
return [];
|
|
3906
|
-
}
|
|
3907
|
-
return this.parseBlame(output);
|
|
3908
|
-
}
|
|
3909
|
-
parseBlame(output) {
|
|
3910
|
-
const lines = output.split("\n");
|
|
3911
|
-
const blameInfo = [];
|
|
3912
|
-
let currentHash = "";
|
|
3913
|
-
let currentAuthor = "";
|
|
3914
|
-
let currentDate = "";
|
|
3915
|
-
let lineNumber = 0;
|
|
3916
|
-
for (const line of lines) {
|
|
3917
|
-
if (line.match(/^[0-9a-f]{6,40}/i)) {
|
|
3918
|
-
const [hash, , finalLine] = line.split(" ");
|
|
3919
|
-
currentHash = hash ?? "";
|
|
3920
|
-
lineNumber = parseInt(finalLine ?? "", 10);
|
|
3921
|
-
} else if (line.startsWith("author ")) {
|
|
3922
|
-
currentAuthor = line.substring(7);
|
|
3923
|
-
} else if (line.startsWith("author-time ")) {
|
|
3924
|
-
const timestamp = parseInt(line.substring(12), 10);
|
|
3925
|
-
currentDate = new Date(timestamp * 1e3).toISOString();
|
|
3926
|
-
} else if (line.startsWith(" ")) {
|
|
3927
|
-
const content = line.substring(1);
|
|
3928
|
-
blameInfo.push({
|
|
3929
|
-
lineNumber,
|
|
3930
|
-
commitHash: currentHash,
|
|
3931
|
-
author: currentAuthor,
|
|
3932
|
-
date: new Date(currentDate),
|
|
3933
|
-
content
|
|
3934
|
-
});
|
|
3935
|
-
}
|
|
3936
|
-
}
|
|
3937
|
-
return blameInfo;
|
|
3938
|
-
}
|
|
3939
|
-
async getAuthors(options = {}) {
|
|
3940
|
-
const args = ["shortlog", "-sne"];
|
|
3941
|
-
if (options.since) {
|
|
3942
|
-
args.push(`--since=${options.since}`);
|
|
3943
|
-
}
|
|
3944
|
-
if (options.until) {
|
|
3945
|
-
args.push(`--until=${options.until}`);
|
|
3946
|
-
}
|
|
3947
|
-
if (!options.includeMerges) {
|
|
3948
|
-
args.push("--no-merges");
|
|
3949
|
-
}
|
|
3950
|
-
if (options.filePath) {
|
|
3951
|
-
args.push("--", options.filePath);
|
|
3952
|
-
}
|
|
3953
|
-
const command = args.join(" ");
|
|
3954
|
-
const output = await executeGit2(command, this.repositoryPath);
|
|
3955
|
-
if (!output) {
|
|
3956
|
-
return [];
|
|
3957
|
-
}
|
|
3958
|
-
return this.parseAuthors(output);
|
|
3959
|
-
}
|
|
3960
|
-
parseAuthors(output) {
|
|
3961
|
-
const lines = output.split("\n").filter((l) => l.trim());
|
|
3962
|
-
const authors = [];
|
|
3963
|
-
for (const line of lines) {
|
|
3964
|
-
const match = line.match(/^\s*(\d+)\s+(.+?)\s+<(.+?)>/);
|
|
3965
|
-
if (match) {
|
|
3966
|
-
const [, commits, name, email] = match;
|
|
3967
|
-
if (commits === void 0 || name === void 0 || email === void 0) continue;
|
|
3968
|
-
authors.push({
|
|
3969
|
-
name,
|
|
3970
|
-
email,
|
|
3971
|
-
commits: parseInt(commits, 10)
|
|
3972
|
-
});
|
|
3973
|
-
}
|
|
3974
|
-
}
|
|
3975
|
-
return authors;
|
|
3976
4155
|
}
|
|
3977
|
-
|
|
3978
|
-
|
|
3979
|
-
|
|
3980
|
-
|
|
3981
|
-
|
|
4156
|
+
const buildable = Object.keys(builders).join(", ");
|
|
4157
|
+
const listed = getAllProviders().map((provider) => provider.id).join(", ");
|
|
4158
|
+
return new Error(
|
|
4159
|
+
`Unknown provider.type "${providerType}" in .taskin.json.
|
|
4160
|
+
Usable now: ${buildable}. Known ids: ${listed}.`
|
|
4161
|
+
);
|
|
4162
|
+
}
|
|
4163
|
+
async function resolveTaskProvider(options = {}, builders = PROVIDER_BUILDERS) {
|
|
4164
|
+
const projectRoot = path7.resolve(options.cwd ?? process.cwd());
|
|
4165
|
+
loadDotEnv(projectRoot);
|
|
4166
|
+
const config = new ConfigManager(projectRoot).loadConfig();
|
|
4167
|
+
const providerType = config.provider.type;
|
|
4168
|
+
const build = builders[providerType];
|
|
4169
|
+
if (!build) {
|
|
4170
|
+
throw unknownProviderError(providerType, builders);
|
|
3982
4171
|
}
|
|
3983
|
-
|
|
4172
|
+
const context = {
|
|
4173
|
+
projectRoot,
|
|
4174
|
+
providerConfig: { ...expandProviderConfig(config.provider.config), ...options.configOverrides },
|
|
4175
|
+
...options.tasksDir !== void 0 && { tasksDirOverride: options.tasksDir }
|
|
4176
|
+
};
|
|
4177
|
+
const { provider, userRegistry } = await build(context);
|
|
4178
|
+
await userRegistry.load();
|
|
4179
|
+
return { provider, userRegistry, projectRoot, providerType };
|
|
4180
|
+
}
|
|
3984
4181
|
|
|
3985
|
-
//
|
|
4182
|
+
// src/lib/provider-factory/provider-factory.types.ts
|
|
3986
4183
|
init_esm_shims();
|
|
3987
4184
|
|
|
3988
|
-
//
|
|
3989
|
-
|
|
3990
|
-
|
|
3991
|
-
|
|
3992
|
-
|
|
3993
|
-
|
|
3994
|
-
}
|
|
3995
|
-
async addFiles(pattern) {
|
|
3996
|
-
try {
|
|
3997
|
-
execSync3(`git add ${pattern}`, {
|
|
3998
|
-
cwd: this.cwd,
|
|
3999
|
-
stdio: "ignore"
|
|
4000
|
-
});
|
|
4001
|
-
return true;
|
|
4002
|
-
} catch {
|
|
4003
|
-
return false;
|
|
4004
|
-
}
|
|
4005
|
-
}
|
|
4006
|
-
async commit(message) {
|
|
4185
|
+
// src/commands/dashboard.ts
|
|
4186
|
+
var __filename2 = fileURLToPath2(import.meta.url);
|
|
4187
|
+
var __dirname2 = path8.dirname(__filename2);
|
|
4188
|
+
async function startHttpServer(app, startPort, host, maxAttempts = 10) {
|
|
4189
|
+
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
4190
|
+
const tryPort = startPort + attempt;
|
|
4007
4191
|
try {
|
|
4008
|
-
|
|
4009
|
-
|
|
4010
|
-
|
|
4192
|
+
const server = createServer(app);
|
|
4193
|
+
await new Promise((resolve, reject) => {
|
|
4194
|
+
server.once("error", reject);
|
|
4195
|
+
server.listen(tryPort, host, () => resolve());
|
|
4011
4196
|
});
|
|
4012
|
-
return
|
|
4013
|
-
} catch {
|
|
4014
|
-
|
|
4015
|
-
|
|
4016
|
-
|
|
4017
|
-
async addAndCommit(pattern, message) {
|
|
4018
|
-
const added = await this.addFiles(pattern);
|
|
4019
|
-
if (!added) return false;
|
|
4020
|
-
return this.commit(message);
|
|
4021
|
-
}
|
|
4022
|
-
async commitTaskStatusChange(taskId, status) {
|
|
4023
|
-
const pattern = `TASKS/task-${taskId}-*.md`;
|
|
4024
|
-
const message = `docs(TASKS): task-${taskId} - atualiza status para ${status} [skip-ci]`;
|
|
4025
|
-
return this.addAndCommit(pattern, message);
|
|
4026
|
-
}
|
|
4027
|
-
async commitTaskStatusChangeOnBranch(taskId, status, defaultBranch) {
|
|
4028
|
-
if (!defaultBranch) {
|
|
4029
|
-
return this.commitTaskStatusChange(taskId, status);
|
|
4030
|
-
}
|
|
4031
|
-
try {
|
|
4032
|
-
const currentBranch = execSync3("git rev-parse --abbrev-ref HEAD", {
|
|
4033
|
-
cwd: this.cwd,
|
|
4034
|
-
encoding: "utf8",
|
|
4035
|
-
stdio: "pipe"
|
|
4036
|
-
}).trim();
|
|
4037
|
-
if (currentBranch === defaultBranch) {
|
|
4038
|
-
return this.commitTaskStatusChange(taskId, status);
|
|
4039
|
-
}
|
|
4040
|
-
const taskPattern = `TASKS/task-${taskId}-*.md`;
|
|
4041
|
-
let taskFileContent = null;
|
|
4042
|
-
let taskFilePath = null;
|
|
4043
|
-
try {
|
|
4044
|
-
const files = execSync3(`git ls-files -m ${taskPattern}`, {
|
|
4045
|
-
cwd: this.cwd,
|
|
4046
|
-
encoding: "utf8",
|
|
4047
|
-
stdio: "pipe"
|
|
4048
|
-
}).trim();
|
|
4049
|
-
if (files) {
|
|
4050
|
-
taskFilePath = files.split("\n")[0] ?? null;
|
|
4051
|
-
const { readFileSync: readFileSync4 } = await import("fs");
|
|
4052
|
-
taskFileContent = readFileSync4(`${this.cwd}/${taskFilePath}`, "utf-8");
|
|
4053
|
-
}
|
|
4054
|
-
} catch {
|
|
4197
|
+
return { server, port: tryPort };
|
|
4198
|
+
} catch (err) {
|
|
4199
|
+
const nodeErr = err;
|
|
4200
|
+
if (nodeErr.code !== "EADDRINUSE") {
|
|
4201
|
+
throw err;
|
|
4055
4202
|
}
|
|
4056
|
-
|
|
4057
|
-
|
|
4058
|
-
try {
|
|
4059
|
-
if (hasChanges) {
|
|
4060
|
-
execSync3('git stash push -u -m "taskin-temp-stash"', {
|
|
4061
|
-
cwd: this.cwd,
|
|
4062
|
-
stdio: "ignore"
|
|
4063
|
-
});
|
|
4064
|
-
stashed = true;
|
|
4065
|
-
}
|
|
4066
|
-
execSync3(`git checkout ${defaultBranch}`, {
|
|
4067
|
-
cwd: this.cwd,
|
|
4068
|
-
stdio: "ignore"
|
|
4069
|
-
});
|
|
4070
|
-
let committed = false;
|
|
4071
|
-
if (taskFileContent && taskFilePath) {
|
|
4072
|
-
const { writeFileSync: writeFileSync3 } = await import("fs");
|
|
4073
|
-
writeFileSync3(`${this.cwd}/${taskFilePath}`, taskFileContent, "utf-8");
|
|
4074
|
-
const message = `docs(TASKS): task-${taskId} - atualiza status para ${status} [skip-ci]`;
|
|
4075
|
-
committed = await this.addAndCommit(taskPattern, message);
|
|
4076
|
-
} else {
|
|
4077
|
-
const message = `docs(TASKS): task-${taskId} - atualiza status para ${status} [skip-ci]`;
|
|
4078
|
-
committed = await this.addAndCommit(taskPattern, message);
|
|
4079
|
-
}
|
|
4080
|
-
execSync3(`git checkout ${currentBranch}`, {
|
|
4081
|
-
cwd: this.cwd,
|
|
4082
|
-
stdio: "ignore"
|
|
4083
|
-
});
|
|
4084
|
-
if (stashed) {
|
|
4085
|
-
execSync3("git stash pop", {
|
|
4086
|
-
cwd: this.cwd,
|
|
4087
|
-
stdio: "ignore"
|
|
4088
|
-
});
|
|
4089
|
-
}
|
|
4090
|
-
return committed;
|
|
4091
|
-
} catch {
|
|
4092
|
-
try {
|
|
4093
|
-
execSync3(`git checkout ${currentBranch}`, {
|
|
4094
|
-
cwd: this.cwd,
|
|
4095
|
-
stdio: "ignore"
|
|
4096
|
-
});
|
|
4097
|
-
if (stashed) {
|
|
4098
|
-
execSync3("git stash pop", {
|
|
4099
|
-
cwd: this.cwd,
|
|
4100
|
-
stdio: "ignore"
|
|
4101
|
-
});
|
|
4102
|
-
}
|
|
4103
|
-
} catch {
|
|
4104
|
-
}
|
|
4105
|
-
return false;
|
|
4203
|
+
if (attempt < maxAttempts - 1) {
|
|
4204
|
+
info(`Port ${tryPort} is in use, trying port ${tryPort + 1}...`);
|
|
4106
4205
|
}
|
|
4107
|
-
} catch {
|
|
4108
|
-
return false;
|
|
4109
4206
|
}
|
|
4110
4207
|
}
|
|
4111
|
-
|
|
4112
|
-
|
|
4113
|
-
|
|
4114
|
-
|
|
4115
|
-
|
|
4116
|
-
|
|
4117
|
-
|
|
4118
|
-
|
|
4119
|
-
|
|
4120
|
-
|
|
4121
|
-
|
|
4208
|
+
throw new Error(
|
|
4209
|
+
`Could not find an available port after ${maxAttempts} attempts (tried ${startPort}-${startPort + maxAttempts - 1})`
|
|
4210
|
+
);
|
|
4211
|
+
}
|
|
4212
|
+
var dashboardCommand = defineCommand({
|
|
4213
|
+
name: "dashboard",
|
|
4214
|
+
description: "\u{1F4CA} Start the Taskin dashboard with WebSocket server",
|
|
4215
|
+
alias: "dash",
|
|
4216
|
+
options: [
|
|
4217
|
+
{
|
|
4218
|
+
flags: "-p, --port <port>",
|
|
4219
|
+
description: "Vite dev server port",
|
|
4220
|
+
defaultValue: "5173"
|
|
4221
|
+
},
|
|
4222
|
+
{
|
|
4223
|
+
flags: "-w, --ws-port <port>",
|
|
4224
|
+
description: "WebSocket server port",
|
|
4225
|
+
defaultValue: "3001"
|
|
4226
|
+
},
|
|
4227
|
+
{
|
|
4228
|
+
flags: "-h, --host <host>",
|
|
4229
|
+
description: "Host to bind servers",
|
|
4230
|
+
defaultValue: "localhost"
|
|
4231
|
+
},
|
|
4232
|
+
{
|
|
4233
|
+
flags: "-b, --browser",
|
|
4234
|
+
description: "Open browser automatically"
|
|
4235
|
+
},
|
|
4236
|
+
{
|
|
4237
|
+
flags: "--open",
|
|
4238
|
+
description: "Show only open tasks (pending, in-progress, blocked)"
|
|
4239
|
+
},
|
|
4240
|
+
{
|
|
4241
|
+
flags: "--closed",
|
|
4242
|
+
description: "Show only closed tasks (done, canceled)"
|
|
4243
|
+
}
|
|
4244
|
+
],
|
|
4245
|
+
handler: async (options) => {
|
|
4246
|
+
await startDashboard(options);
|
|
4247
|
+
}
|
|
4248
|
+
});
|
|
4249
|
+
async function startDashboard(options) {
|
|
4250
|
+
requireTaskinProject();
|
|
4251
|
+
const host = options.host || "localhost";
|
|
4252
|
+
if (!isValidHost(host)) {
|
|
4253
|
+
error("Security validation failed");
|
|
4254
|
+
error(`Invalid host: ${host}. Must be localhost, a valid IPv4 address, or hostname.`);
|
|
4255
|
+
process.exit(1);
|
|
4122
4256
|
}
|
|
4123
|
-
|
|
4124
|
-
|
|
4125
|
-
|
|
4126
|
-
|
|
4127
|
-
encoding: "utf8",
|
|
4128
|
-
stdio: "pipe"
|
|
4129
|
-
}).trim();
|
|
4130
|
-
} catch {
|
|
4131
|
-
return "";
|
|
4132
|
-
}
|
|
4257
|
+
if (typeof options.port === "string" && !isValidPort(options.port)) {
|
|
4258
|
+
error("Security validation failed");
|
|
4259
|
+
error(`Invalid port: ${options.port}. Must be between 1 and 65535.`);
|
|
4260
|
+
process.exit(1);
|
|
4133
4261
|
}
|
|
4134
|
-
|
|
4135
|
-
|
|
4262
|
+
if (typeof options.wsPort === "string" && !isValidPort(options.wsPort)) {
|
|
4263
|
+
error("Security validation failed");
|
|
4264
|
+
error(`Invalid WebSocket port: ${options.wsPort}. Must be between 1 and 65535.`);
|
|
4265
|
+
process.exit(1);
|
|
4136
4266
|
}
|
|
4137
|
-
|
|
4138
|
-
|
|
4139
|
-
|
|
4140
|
-
|
|
4141
|
-
}
|
|
4142
|
-
|
|
4143
|
-
}
|
|
4267
|
+
const port = typeof options.port === "string" ? parseInt(options.port, 10) : options.port || 5173;
|
|
4268
|
+
const wsPort = typeof options.wsPort === "string" ? parseInt(options.wsPort, 10) : options.wsPort || 3001;
|
|
4269
|
+
if (!isValidPort(port)) {
|
|
4270
|
+
error("Security validation failed");
|
|
4271
|
+
error(`Invalid port: ${port}. Must be between 1 and 65535.`);
|
|
4272
|
+
process.exit(1);
|
|
4144
4273
|
}
|
|
4145
|
-
|
|
4146
|
-
|
|
4147
|
-
|
|
4148
|
-
|
|
4149
|
-
stdio: "ignore"
|
|
4150
|
-
});
|
|
4151
|
-
return true;
|
|
4152
|
-
} catch {
|
|
4153
|
-
return false;
|
|
4154
|
-
}
|
|
4274
|
+
if (!isValidPort(wsPort)) {
|
|
4275
|
+
error("Security validation failed");
|
|
4276
|
+
error(`Invalid WebSocket port: ${wsPort}. Must be between 1 and 65535.`);
|
|
4277
|
+
process.exit(1);
|
|
4155
4278
|
}
|
|
4156
|
-
|
|
4279
|
+
printHeader("Starting Taskin Dashboard", "\u{1F4CA}");
|
|
4280
|
+
try {
|
|
4281
|
+
info("Initializing task provider...");
|
|
4282
|
+
let currentDir = process.cwd();
|
|
4283
|
+
let tasksDir = path8.join(currentDir, "TASKS");
|
|
4157
4284
|
try {
|
|
4158
|
-
|
|
4159
|
-
cwd: this.cwd,
|
|
4160
|
-
stdio: "ignore"
|
|
4161
|
-
});
|
|
4162
|
-
return true;
|
|
4285
|
+
await import("fs").then((fs6) => fs6.promises.access(tasksDir));
|
|
4163
4286
|
} catch {
|
|
4164
|
-
|
|
4287
|
+
while (currentDir !== path8.dirname(currentDir)) {
|
|
4288
|
+
const workspaceFile = path8.join(currentDir, "pnpm-workspace.yaml");
|
|
4289
|
+
try {
|
|
4290
|
+
await import("fs").then((fs6) => fs6.promises.access(workspaceFile));
|
|
4291
|
+
tasksDir = path8.join(currentDir, "TASKS");
|
|
4292
|
+
break;
|
|
4293
|
+
} catch {
|
|
4294
|
+
currentDir = path8.dirname(currentDir);
|
|
4295
|
+
}
|
|
4296
|
+
}
|
|
4165
4297
|
}
|
|
4166
|
-
|
|
4167
|
-
|
|
4168
|
-
|
|
4169
|
-
|
|
4170
|
-
|
|
4171
|
-
|
|
4172
|
-
|
|
4173
|
-
|
|
4174
|
-
|
|
4175
|
-
|
|
4298
|
+
info(`Using tasks directory: ${tasksDir}`);
|
|
4299
|
+
const monorepoRoot = path8.dirname(tasksDir);
|
|
4300
|
+
const { provider } = await resolveTaskProvider({ cwd: monorepoRoot });
|
|
4301
|
+
const manager = new TaskManager(provider);
|
|
4302
|
+
info(`Starting WebSocket server on ${host}:${wsPort}...`);
|
|
4303
|
+
const wsServer = new TaskWebSocketServer({
|
|
4304
|
+
taskManager: manager,
|
|
4305
|
+
taskProvider: provider,
|
|
4306
|
+
options: {
|
|
4307
|
+
port: wsPort,
|
|
4308
|
+
host
|
|
4309
|
+
}
|
|
4310
|
+
});
|
|
4311
|
+
await wsServer.start();
|
|
4312
|
+
success(`\u2713 WebSocket server running on ws://${host}:${wsPort}`);
|
|
4313
|
+
info(`Starting dashboard server on http://${host}:${port}...`);
|
|
4314
|
+
const isDev = __dirname2.includes("/src/");
|
|
4315
|
+
const dashboardDist = isDev ? path8.join(__dirname2, "..", "..", "dashboard-dist") : path8.join(__dirname2, "..", "dashboard-dist");
|
|
4316
|
+
const app = express();
|
|
4317
|
+
app.disable("x-powered-by");
|
|
4318
|
+
app.use((_req, res, next) => {
|
|
4319
|
+
res.setHeader("X-Frame-Options", "DENY");
|
|
4320
|
+
res.setHeader("X-Content-Type-Options", "nosniff");
|
|
4321
|
+
res.setHeader("X-XSS-Protection", "1; mode=block");
|
|
4322
|
+
res.setHeader(
|
|
4323
|
+
"Content-Security-Policy",
|
|
4324
|
+
"default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; connect-src 'self' ws: wss:;"
|
|
4325
|
+
);
|
|
4326
|
+
next();
|
|
4327
|
+
});
|
|
4328
|
+
app.use((req, res, next) => {
|
|
4329
|
+
if (req.path === "/" || req.path === "/index.html") {
|
|
4330
|
+
import("fs").then((fs6) => fs6.promises.readFile(path8.join(dashboardDist, "index.html"), "utf-8")).then((html) => {
|
|
4331
|
+
const safeHost = escapeHtml(host);
|
|
4332
|
+
const safeWsPort = escapeHtml(String(wsPort));
|
|
4333
|
+
const injectedHtml = html.replace(
|
|
4334
|
+
"</head>",
|
|
4335
|
+
`<script>window.VITE_WS_URL = 'ws://${safeHost}:${safeWsPort}';</script></head>`
|
|
4336
|
+
);
|
|
4337
|
+
res.send(injectedHtml);
|
|
4338
|
+
}).catch((err) => {
|
|
4339
|
+
console.error("Failed to read index.html:", err);
|
|
4340
|
+
res.status(500).send("Internal Server Error");
|
|
4341
|
+
});
|
|
4342
|
+
} else {
|
|
4343
|
+
next();
|
|
4344
|
+
}
|
|
4345
|
+
});
|
|
4346
|
+
app.use(
|
|
4347
|
+
express.static(dashboardDist, {
|
|
4348
|
+
dotfiles: "deny",
|
|
4349
|
+
// Deny access to dotfiles
|
|
4350
|
+
index: false,
|
|
4351
|
+
// Don't serve index.html here (handled above)
|
|
4352
|
+
redirect: false
|
|
4353
|
+
// Don't redirect to trailing slash
|
|
4354
|
+
})
|
|
4355
|
+
);
|
|
4356
|
+
app.use((_req, res) => {
|
|
4357
|
+
res.status(404).send("Not Found");
|
|
4358
|
+
});
|
|
4359
|
+
const { server: httpServer, port: actualPort } = await startHttpServer(app, port, host);
|
|
4360
|
+
if (actualPort !== port) {
|
|
4361
|
+
warning(`Port ${port} was in use. Dashboard started on port ${actualPort}.`);
|
|
4176
4362
|
}
|
|
4177
|
-
|
|
4178
|
-
|
|
4179
|
-
|
|
4180
|
-
|
|
4181
|
-
|
|
4182
|
-
|
|
4183
|
-
});
|
|
4184
|
-
return true;
|
|
4185
|
-
} catch {
|
|
4186
|
-
return false;
|
|
4363
|
+
success(`\u2713 Dashboard available at http://${host}:${actualPort}`);
|
|
4364
|
+
const filterParams = new URLSearchParams();
|
|
4365
|
+
if (options.open) {
|
|
4366
|
+
filterParams.set("filter", "open");
|
|
4367
|
+
} else if (options.closed) {
|
|
4368
|
+
filterParams.set("filter", "closed");
|
|
4187
4369
|
}
|
|
4188
|
-
|
|
4189
|
-
|
|
4190
|
-
|
|
4191
|
-
|
|
4192
|
-
|
|
4193
|
-
|
|
4370
|
+
const filterQuery = filterParams.toString() ? `?${filterParams.toString()}` : "";
|
|
4371
|
+
if (options.browser) {
|
|
4372
|
+
const url = `http://${host}:${actualPort}${filterQuery}`;
|
|
4373
|
+
await import("child_process").then((cp) => {
|
|
4374
|
+
const cmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
|
|
4375
|
+
cp.exec(`${cmd} ${url}`);
|
|
4194
4376
|
});
|
|
4195
|
-
return true;
|
|
4196
|
-
} catch {
|
|
4197
|
-
return false;
|
|
4198
4377
|
}
|
|
4199
|
-
|
|
4200
|
-
|
|
4201
|
-
|
|
4202
|
-
|
|
4203
|
-
|
|
4204
|
-
|
|
4378
|
+
info("");
|
|
4379
|
+
info(chalk4.bold("Dashboard Controls:"));
|
|
4380
|
+
info(` \u2022 Dashboard: ${chalk4.cyan(`http://${host}:${actualPort}${filterQuery}`)}`);
|
|
4381
|
+
info(` \u2022 WebSocket: ${chalk4.cyan(`ws://${host}:${wsPort}`)}`);
|
|
4382
|
+
if (options.open) {
|
|
4383
|
+
info(` \u2022 Filter: ${chalk4.yellow("Open tasks only")}`);
|
|
4384
|
+
} else if (options.closed) {
|
|
4385
|
+
info(` \u2022 Filter: ${chalk4.yellow("Closed tasks only")}`);
|
|
4386
|
+
}
|
|
4387
|
+
info(` \u2022 Press ${chalk4.bold("Ctrl+C")} to stop both servers`);
|
|
4388
|
+
info("");
|
|
4389
|
+
const cleanup = async () => {
|
|
4390
|
+
info("\nShutting down servers...");
|
|
4391
|
+
await new Promise((resolve) => {
|
|
4392
|
+
httpServer.close(() => resolve());
|
|
4205
4393
|
});
|
|
4206
|
-
|
|
4207
|
-
|
|
4208
|
-
|
|
4394
|
+
await wsServer.stop();
|
|
4395
|
+
success("\u2713 Servers stopped");
|
|
4396
|
+
process.exit(0);
|
|
4397
|
+
};
|
|
4398
|
+
process.on("SIGINT", cleanup);
|
|
4399
|
+
process.on("SIGTERM", cleanup);
|
|
4400
|
+
} catch (err) {
|
|
4401
|
+
error("Failed to start dashboard");
|
|
4402
|
+
if (err instanceof Error) {
|
|
4403
|
+
error(err.message);
|
|
4209
4404
|
}
|
|
4405
|
+
process.exit(1);
|
|
4210
4406
|
}
|
|
4211
|
-
}
|
|
4212
|
-
|
|
4213
|
-
// ../git-utils/src/git-service.types.ts
|
|
4214
|
-
init_esm_shims();
|
|
4407
|
+
}
|
|
4215
4408
|
|
|
4216
4409
|
// src/commands/export.ts
|
|
4410
|
+
init_esm_shims();
|
|
4411
|
+
init_src3();
|
|
4412
|
+
init_src();
|
|
4217
4413
|
import fs5 from "fs/promises";
|
|
4218
4414
|
import path9 from "path";
|
|
4219
4415
|
function userStatsToCsv(stats) {
|
|
@@ -4291,7 +4487,8 @@ function registerExportCommand(program2) {
|
|
|
4291
4487
|
|
|
4292
4488
|
// src/commands/finish.ts
|
|
4293
4489
|
init_esm_shims();
|
|
4294
|
-
|
|
4490
|
+
init_src3();
|
|
4491
|
+
init_src();
|
|
4295
4492
|
import { execSync as execSync5 } from "child_process";
|
|
4296
4493
|
|
|
4297
4494
|
// src/lib/notification/notify-helper.ts
|
|
@@ -4713,6 +4910,14 @@ async function finishTask(taskId, options, gitService) {
|
|
|
4713
4910
|
}
|
|
4714
4911
|
info(`Found task: ${task.title}`);
|
|
4715
4912
|
info(`Current status: ${task.status}`);
|
|
4913
|
+
const configManager = new ConfigManager(monorepoRoot);
|
|
4914
|
+
const behavior = configManager.getAutomationBehavior();
|
|
4915
|
+
const autoSyncActive = behavior.autoSync && !!behavior.defaultBranch;
|
|
4916
|
+
const statusCommitMessage = buildTaskStatusCommitMessage({
|
|
4917
|
+
taskId: normalizedId,
|
|
4918
|
+
status: "done",
|
|
4919
|
+
ciSkipTag: behavior.ciSkipTag
|
|
4920
|
+
});
|
|
4716
4921
|
if (options.dryRun) {
|
|
4717
4922
|
console.log();
|
|
4718
4923
|
info("\u{1F50D} Dry run mode - showing what would be executed:");
|
|
@@ -4724,7 +4929,7 @@ async function finishTask(taskId, options, gitService) {
|
|
|
4724
4929
|
info("Git operations:");
|
|
4725
4930
|
console.log(
|
|
4726
4931
|
colors.secondary(
|
|
4727
|
-
` - Commit status: git add TASKS/task-${normalizedId}-*.md && git commit -m "
|
|
4932
|
+
` - Commit status: git add TASKS/task-${normalizedId}-*.md && git commit -m "${statusCommitMessage}"`
|
|
4728
4933
|
)
|
|
4729
4934
|
);
|
|
4730
4935
|
console.log(
|
|
@@ -4741,13 +4946,10 @@ async function finishTask(taskId, options, gitService) {
|
|
|
4741
4946
|
error("Task is already done");
|
|
4742
4947
|
process.exit(1);
|
|
4743
4948
|
}
|
|
4744
|
-
const configManager = new ConfigManager(monorepoRoot);
|
|
4745
|
-
const behavior = configManager.getAutomationBehavior();
|
|
4746
|
-
const autoSyncActive = behavior.autoSync && !!behavior.defaultBranch;
|
|
4747
4949
|
if (behavior.autoSync && !behavior.defaultBranch) {
|
|
4748
4950
|
warning("autoSync is enabled but no defaultBranch is configured. Nothing will be synced.");
|
|
4749
4951
|
}
|
|
4750
|
-
const git = gitService ?? new GitService(process.cwd());
|
|
4952
|
+
const git = gitService ?? new GitService(process.cwd(), { ciSkipTag: behavior.ciSkipTag });
|
|
4751
4953
|
if (!options.skipUpdate) {
|
|
4752
4954
|
info("Marking task as done...");
|
|
4753
4955
|
const updatedTask = await taskManager.finishTask(task.id);
|
|
@@ -4764,7 +4966,8 @@ async function finishTask(taskId, options, gitService) {
|
|
|
4764
4966
|
const squashed = await squashTaskFileOnDone(git, {
|
|
4765
4967
|
taskId: normalizedId,
|
|
4766
4968
|
defaultBranch: behavior.defaultBranch,
|
|
4767
|
-
originBranch: behavior.originBranch
|
|
4969
|
+
originBranch: behavior.originBranch,
|
|
4970
|
+
ciSkipTag: behavior.ciSkipTag
|
|
4768
4971
|
});
|
|
4769
4972
|
if (squashed) {
|
|
4770
4973
|
success(`\u2713 Squash commit pushed to ${behavior.originBranch}`);
|
|
@@ -4798,7 +5001,7 @@ async function finishTask(taskId, options, gitService) {
|
|
|
4798
5001
|
if (!options.skipUpdate) {
|
|
4799
5002
|
if (!behavior.autoCommitStatusChange) {
|
|
4800
5003
|
steps.push(
|
|
4801
|
-
`Commit the status change: git add TASKS/task-${normalizedId}-*.md && git commit -m "
|
|
5004
|
+
`Commit the status change: git add TASKS/task-${normalizedId}-*.md && git commit -m "${statusCommitMessage}"`
|
|
4802
5005
|
);
|
|
4803
5006
|
}
|
|
4804
5007
|
steps.push("Review your changes");
|
|
@@ -4839,6 +5042,7 @@ async function finishTask(taskId, options, gitService) {
|
|
|
4839
5042
|
|
|
4840
5043
|
// src/commands/init.ts
|
|
4841
5044
|
init_esm_shims();
|
|
5045
|
+
init_src();
|
|
4842
5046
|
import { existsSync as existsSync5, readdirSync, writeFileSync as writeFileSync2 } from "fs";
|
|
4843
5047
|
import inquirer2 from "inquirer";
|
|
4844
5048
|
import { join as join3 } from "path";
|
|
@@ -4929,6 +5133,7 @@ async function ensureProviderInstalled(provider) {
|
|
|
4929
5133
|
init_esm_shims();
|
|
4930
5134
|
|
|
4931
5135
|
// src/commands/init.ts
|
|
5136
|
+
var NO_CI_SKIP_TAG_KEYWORD2 = "none";
|
|
4932
5137
|
var initCommand = defineCommand({
|
|
4933
5138
|
name: "init",
|
|
4934
5139
|
alias: "setup",
|
|
@@ -4941,6 +5146,10 @@ var initCommand = defineCommand({
|
|
|
4941
5146
|
{
|
|
4942
5147
|
flags: "-p, --provider <provider>",
|
|
4943
5148
|
description: "Provider to use (fs, redmine, jira, github) - skips interactive prompt"
|
|
5149
|
+
},
|
|
5150
|
+
{
|
|
5151
|
+
flags: "--ci-skip-tag <tag>",
|
|
5152
|
+
description: `Tag appended to Taskin's own commits so they skip CI (default "${DEFAULT_CI_SKIP_TAG}"; "none" to run CI) - skips interactive prompt`
|
|
4944
5153
|
}
|
|
4945
5154
|
],
|
|
4946
5155
|
handler: async (options) => {
|
|
@@ -4999,8 +5208,14 @@ async function initializeTaskin(options) {
|
|
|
4999
5208
|
await ensureProviderInstalled(selectedProvider);
|
|
5000
5209
|
}
|
|
5001
5210
|
const providerConfig = await setupProviderConfig(selectedProvider, cwd);
|
|
5211
|
+
const ciSkipTag = await resolveCiSkipTag(options.ciSkipTag);
|
|
5002
5212
|
const config = {
|
|
5003
5213
|
version: "1.0.3",
|
|
5214
|
+
automation: {
|
|
5215
|
+
level: "assisted",
|
|
5216
|
+
autoSync: true,
|
|
5217
|
+
ciSkipTag
|
|
5218
|
+
},
|
|
5004
5219
|
provider: {
|
|
5005
5220
|
type: selectedProvider.id,
|
|
5006
5221
|
config: providerConfig
|
|
@@ -5027,6 +5242,51 @@ async function initializeTaskin(options) {
|
|
|
5027
5242
|
info("For more information, run: taskin --help");
|
|
5028
5243
|
console.log();
|
|
5029
5244
|
}
|
|
5245
|
+
async function resolveCiSkipTag(fromFlag) {
|
|
5246
|
+
if (fromFlag !== void 0) {
|
|
5247
|
+
return normalizeCiSkipTag(fromFlag);
|
|
5248
|
+
}
|
|
5249
|
+
if (process.env.CI === "true") {
|
|
5250
|
+
return DEFAULT_CI_SKIP_TAG;
|
|
5251
|
+
}
|
|
5252
|
+
console.log();
|
|
5253
|
+
info("Taskin appends a tag to the commits it writes itself \u2014 status changes and");
|
|
5254
|
+
info("task files \u2014 so they do not trigger your pipeline.");
|
|
5255
|
+
console.log();
|
|
5256
|
+
const { choice } = await inquirer2.prompt([
|
|
5257
|
+
{
|
|
5258
|
+
type: "list",
|
|
5259
|
+
name: "choice",
|
|
5260
|
+
message: "Tag for Taskin commits:",
|
|
5261
|
+
default: DEFAULT_CI_SKIP_TAG,
|
|
5262
|
+
choices: [
|
|
5263
|
+
{ name: `${CI_SKIP_TAGS[0]} \u2014 GitHub, GitLab and Bitbucket (recommended)`, value: CI_SKIP_TAGS[0] },
|
|
5264
|
+
{ name: `${CI_SKIP_TAGS[1]} \u2014 GitHub, GitLab and Bitbucket`, value: CI_SKIP_TAGS[1] },
|
|
5265
|
+
{ name: `${CI_SKIP_TAGS[2]} \u2014 GitHub Actions only`, value: CI_SKIP_TAGS[2] },
|
|
5266
|
+
{ name: `${CI_SKIP_TAGS[3]} \u2014 GitHub Actions only`, value: CI_SKIP_TAGS[3] },
|
|
5267
|
+
{ name: `${CI_SKIP_TAGS[4]} \u2014 GitHub Actions only`, value: CI_SKIP_TAGS[4] },
|
|
5268
|
+
{ name: "none \u2014 do not mark the commits, let CI run", value: NO_CI_SKIP_TAG_KEYWORD2 },
|
|
5269
|
+
{ name: "custom\u2026 \u2014 another CI (Azure DevOps uses ***NO_CI***)", value: "custom" }
|
|
5270
|
+
]
|
|
5271
|
+
}
|
|
5272
|
+
]);
|
|
5273
|
+
if (choice !== "custom") {
|
|
5274
|
+
return normalizeCiSkipTag(choice);
|
|
5275
|
+
}
|
|
5276
|
+
const { customTag } = await inquirer2.prompt([
|
|
5277
|
+
{
|
|
5278
|
+
type: "input",
|
|
5279
|
+
name: "customTag",
|
|
5280
|
+
message: "Tag to append:",
|
|
5281
|
+
default: DEFAULT_CI_SKIP_TAG
|
|
5282
|
+
}
|
|
5283
|
+
]);
|
|
5284
|
+
return normalizeCiSkipTag(customTag);
|
|
5285
|
+
}
|
|
5286
|
+
function normalizeCiSkipTag(input) {
|
|
5287
|
+
const trimmed = input.trim();
|
|
5288
|
+
return trimmed.toLowerCase() === NO_CI_SKIP_TAG_KEYWORD2 ? "" : trimmed;
|
|
5289
|
+
}
|
|
5030
5290
|
async function setupProviderConfig(provider, cwd) {
|
|
5031
5291
|
if (provider.id === "fs") {
|
|
5032
5292
|
return setupFileSystemProvider(cwd);
|
|
@@ -5051,7 +5311,7 @@ async function setupProviderConfig(provider, cwd) {
|
|
|
5051
5311
|
}
|
|
5052
5312
|
async function setupFileSystemProvider(cwd) {
|
|
5053
5313
|
const tasksDir = join3(cwd, "TASKS");
|
|
5054
|
-
const { DEFAULT_METADATA_STYLE_ID: DEFAULT_METADATA_STYLE_ID2, FileSystemTaskProvider: FileSystemTaskProvider2, getMetadataStyle: getMetadataStyle2, UserRegistry: UserRegistry2 } = await Promise.resolve().then(() => (
|
|
5314
|
+
const { DEFAULT_METADATA_STYLE_ID: DEFAULT_METADATA_STYLE_ID2, FileSystemTaskProvider: FileSystemTaskProvider2, getMetadataStyle: getMetadataStyle2, UserRegistry: UserRegistry2 } = await Promise.resolve().then(() => (init_src3(), src_exports));
|
|
5055
5315
|
const userRegistry = new UserRegistry2({ taskinDir: join3(cwd, ".taskin") });
|
|
5056
5316
|
const fileSystemProvider = new FileSystemTaskProvider2(tasksDir, userRegistry);
|
|
5057
5317
|
await fileSystemProvider.initialize();
|
|
@@ -5096,7 +5356,7 @@ You can edit or delete this file. Use \`taskin list\` to see all tasks.
|
|
|
5096
5356
|
};
|
|
5097
5357
|
}
|
|
5098
5358
|
async function promptCreateFirstUser(cwd) {
|
|
5099
|
-
const { UserRegistry: UserRegistry2 } = await Promise.resolve().then(() => (
|
|
5359
|
+
const { UserRegistry: UserRegistry2 } = await Promise.resolve().then(() => (init_src3(), src_exports));
|
|
5100
5360
|
const taskinDir = join3(cwd, ".taskin");
|
|
5101
5361
|
const userRegistry = new UserRegistry2({ taskinDir });
|
|
5102
5362
|
const { createFirstUser } = await inquirer2.prompt([
|
|
@@ -5862,7 +6122,8 @@ async function startMCPServer(options) {
|
|
|
5862
6122
|
|
|
5863
6123
|
// src/commands/new.ts
|
|
5864
6124
|
init_esm_shims();
|
|
5865
|
-
|
|
6125
|
+
init_src3();
|
|
6126
|
+
init_src();
|
|
5866
6127
|
import { TASK_TYPES as TASK_TYPES2 } from "@opentask/taskin-types";
|
|
5867
6128
|
import inquirer3 from "inquirer";
|
|
5868
6129
|
var createCommand = defineCommand({
|
|
@@ -5964,7 +6225,7 @@ async function createTask(options, gitService) {
|
|
|
5964
6225
|
if (behavior.autoSync && !behavior.defaultBranch) {
|
|
5965
6226
|
warning("autoSync is enabled but no defaultBranch is configured. Nothing will be synced.");
|
|
5966
6227
|
}
|
|
5967
|
-
const git = gitService ?? new GitService(process.cwd());
|
|
6228
|
+
const git = gitService ?? new GitService(process.cwd(), { ciSkipTag: behavior.ciSkipTag });
|
|
5968
6229
|
if (autoSyncActive) {
|
|
5969
6230
|
try {
|
|
5970
6231
|
await syncBeforeCreate(git, {
|
|
@@ -5996,7 +6257,8 @@ async function createTask(options, gitService) {
|
|
|
5996
6257
|
await pushAfterCreate(git, {
|
|
5997
6258
|
taskId,
|
|
5998
6259
|
title: options.title,
|
|
5999
|
-
defaultBranch: behavior.defaultBranch
|
|
6260
|
+
defaultBranch: behavior.defaultBranch,
|
|
6261
|
+
ciSkipTag: behavior.ciSkipTag
|
|
6000
6262
|
});
|
|
6001
6263
|
} catch (pushError) {
|
|
6002
6264
|
error(
|
|
@@ -6216,6 +6478,7 @@ async function pauseTask(taskId, options) {
|
|
|
6216
6478
|
|
|
6217
6479
|
// src/commands/review.ts
|
|
6218
6480
|
init_esm_shims();
|
|
6481
|
+
init_src();
|
|
6219
6482
|
import { execSync as execSync9 } from "child_process";
|
|
6220
6483
|
|
|
6221
6484
|
// src/lib/hook-runner.ts
|
|
@@ -6441,10 +6704,14 @@ async function reviewTask(taskId, options) {
|
|
|
6441
6704
|
success(`\u2713 Task ${updatedTask.id} status changed to: ${updatedTask.status}`);
|
|
6442
6705
|
if (behavior.autoCommitStatusChange) {
|
|
6443
6706
|
try {
|
|
6444
|
-
|
|
6445
|
-
`
|
|
6446
|
-
|
|
6707
|
+
const message = appendCiSkipTag(
|
|
6708
|
+
`docs(TASKS): task-${normalizedId} - mark as ready for review`,
|
|
6709
|
+
behavior.ciSkipTag
|
|
6447
6710
|
);
|
|
6711
|
+
execSync9(`git add TASKS/task-${normalizedId}-*.md && git commit -m "${message}"`, {
|
|
6712
|
+
cwd: monorepoRoot,
|
|
6713
|
+
stdio: "ignore"
|
|
6714
|
+
});
|
|
6448
6715
|
success("\u2713 Auto-committed status change");
|
|
6449
6716
|
} catch {
|
|
6450
6717
|
}
|
|
@@ -6492,6 +6759,7 @@ async function reviewTask(taskId, options) {
|
|
|
6492
6759
|
|
|
6493
6760
|
// src/commands/start.ts
|
|
6494
6761
|
init_esm_shims();
|
|
6762
|
+
init_src();
|
|
6495
6763
|
var startCommand = defineCommand({
|
|
6496
6764
|
name: "start <task-id>",
|
|
6497
6765
|
description: "\u{1F680} Start working on a task",
|
|
@@ -6536,6 +6804,13 @@ async function startTask(taskId, _options, gitService) {
|
|
|
6536
6804
|
}
|
|
6537
6805
|
info(`Found task: ${task.title}`);
|
|
6538
6806
|
info(`Current status: ${task.status}`);
|
|
6807
|
+
const configManager = new ConfigManager(monorepoRoot);
|
|
6808
|
+
const behavior = configManager.getAutomationBehavior();
|
|
6809
|
+
const statusCommitMessage = buildTaskStatusCommitMessage({
|
|
6810
|
+
taskId: normalizedId,
|
|
6811
|
+
status: "in-progress",
|
|
6812
|
+
ciSkipTag: behavior.ciSkipTag
|
|
6813
|
+
});
|
|
6539
6814
|
if (_options.dryRun) {
|
|
6540
6815
|
console.log();
|
|
6541
6816
|
info("\u{1F50D} Dry run mode - showing what would be executed:");
|
|
@@ -6547,7 +6822,7 @@ async function startTask(taskId, _options, gitService) {
|
|
|
6547
6822
|
console.log(colors.secondary(` - Create branch: git checkout -b feat/task-${normalizedId}`));
|
|
6548
6823
|
console.log(
|
|
6549
6824
|
colors.secondary(
|
|
6550
|
-
` - Commit status: git add TASKS/task-${normalizedId}-*.md && git commit -m "
|
|
6825
|
+
` - Commit status: git add TASKS/task-${normalizedId}-*.md && git commit -m "${statusCommitMessage}"`
|
|
6551
6826
|
)
|
|
6552
6827
|
);
|
|
6553
6828
|
console.log();
|
|
@@ -6566,9 +6841,7 @@ async function startTask(taskId, _options, gitService) {
|
|
|
6566
6841
|
const updatedTask = await taskManager.startTask(task.id);
|
|
6567
6842
|
success(`Task ${updatedTask.id} started successfully!`);
|
|
6568
6843
|
success(`Status changed to: ${updatedTask.status}`);
|
|
6569
|
-
const
|
|
6570
|
-
const behavior = configManager.getAutomationBehavior();
|
|
6571
|
-
const git = gitService ?? new GitService(process.cwd());
|
|
6844
|
+
const git = gitService ?? new GitService(process.cwd(), { ciSkipTag: behavior.ciSkipTag });
|
|
6572
6845
|
if (behavior.autoCommitStatusChange) {
|
|
6573
6846
|
const committed = await git.commitTaskStatusChangeOnBranch(normalizedId, "in-progress", behavior.defaultBranch);
|
|
6574
6847
|
if (committed) {
|
|
@@ -6580,7 +6853,7 @@ async function startTask(taskId, _options, gitService) {
|
|
|
6580
6853
|
info("Next steps (suggestions):");
|
|
6581
6854
|
console.log(
|
|
6582
6855
|
colors.secondary(
|
|
6583
|
-
` 1. Commit the status change: git add TASKS/task-${normalizedId}-*.md && git commit -m "
|
|
6856
|
+
` 1. Commit the status change: git add TASKS/task-${normalizedId}-*.md && git commit -m "${statusCommitMessage}"`
|
|
6584
6857
|
)
|
|
6585
6858
|
);
|
|
6586
6859
|
console.log(colors.secondary(` 2. Create a branch: git checkout -b feat/task-${normalizedId}`));
|
|
@@ -6601,7 +6874,8 @@ async function startTask(taskId, _options, gitService) {
|
|
|
6601
6874
|
|
|
6602
6875
|
// src/commands/stats.ts
|
|
6603
6876
|
init_esm_shims();
|
|
6604
|
-
|
|
6877
|
+
init_src3();
|
|
6878
|
+
init_src();
|
|
6605
6879
|
import chalk7 from "chalk";
|
|
6606
6880
|
import path11 from "path";
|
|
6607
6881
|
var statsCommand = defineCommand({
|
|
@@ -6943,7 +7217,7 @@ init_esm_shims();
|
|
|
6943
7217
|
|
|
6944
7218
|
// src/lib/file-system-task-linter/file-system-task-linter.ts
|
|
6945
7219
|
init_esm_shims();
|
|
6946
|
-
|
|
7220
|
+
init_src3();
|
|
6947
7221
|
import { TASK_STATUSES as TASK_STATUSES3, TASK_TYPES as TASK_TYPES3 } from "@opentask/taskin-types";
|
|
6948
7222
|
import chalk8 from "chalk";
|
|
6949
7223
|
import { readdir, readFile as readFile2 } from "fs/promises";
|