taskin 4.1.0 → 4.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/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/);
|
|
@@ -673,89 +1113,365 @@ var init_metadata_style2 = __esm({
|
|
|
673
1113
|
}
|
|
674
1114
|
});
|
|
675
1115
|
|
|
676
|
-
// ../file-system-task-provider/src/
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
}
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
1116
|
+
// ../file-system-task-provider/src/assignee-identity.ts
|
|
1117
|
+
function classifyAssignee(raw, registry) {
|
|
1118
|
+
const trimmed = raw.trim();
|
|
1119
|
+
if (trimmed === "" || PLACEHOLDER_ASSIGNEES.includes(trimmed.toLowerCase())) {
|
|
1120
|
+
return { kind: "unassigned", raw };
|
|
1121
|
+
}
|
|
1122
|
+
const user = registry.resolveUser(trimmed);
|
|
1123
|
+
if (user) {
|
|
1124
|
+
return { kind: "resolved", raw, user };
|
|
1125
|
+
}
|
|
1126
|
+
const folded = fold(trimmed);
|
|
1127
|
+
const matches = registry.getAllUsers().filter((candidate) => fold(candidate.id) === folded || fold(candidate.name) === folded);
|
|
1128
|
+
const [onlyMatch] = matches;
|
|
1129
|
+
if (onlyMatch && matches.length === 1) {
|
|
1130
|
+
return { kind: "correctable", raw, user: onlyMatch };
|
|
1131
|
+
}
|
|
1132
|
+
return { kind: "unknown", raw };
|
|
686
1133
|
}
|
|
687
|
-
function
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
1134
|
+
function validateAssignees(tasks, registry) {
|
|
1135
|
+
const issues = [];
|
|
1136
|
+
for (const task of tasks) {
|
|
1137
|
+
if (task.assignee === void 0) continue;
|
|
1138
|
+
const identity = classifyAssignee(task.assignee, registry);
|
|
1139
|
+
switch (identity.kind) {
|
|
1140
|
+
case "resolved":
|
|
1141
|
+
case "unassigned":
|
|
1142
|
+
break;
|
|
1143
|
+
case "correctable":
|
|
1144
|
+
issues.push({
|
|
1145
|
+
file: task.file,
|
|
1146
|
+
message: `Assignee "${identity.raw.trim()}" is not in the user registry, but folds onto exactly one registered user.`,
|
|
1147
|
+
severity: "warning",
|
|
1148
|
+
suggestion: `Rewrite it as "${identity.user.id}" \u2014 lint --fix does this.`
|
|
1149
|
+
});
|
|
1150
|
+
break;
|
|
1151
|
+
case "unknown":
|
|
1152
|
+
issues.push({
|
|
1153
|
+
file: task.file,
|
|
1154
|
+
message: `Assignee "${identity.raw.trim()}" resolves to nobody in the user registry, so it silently becomes a fabricated temporary user.`,
|
|
1155
|
+
severity: "warning",
|
|
1156
|
+
suggestion: `Register them in ${".taskin/.taskin-users.json"}, or fix the spelling \u2014 too ambiguous for --fix to decide.`
|
|
1157
|
+
});
|
|
1158
|
+
break;
|
|
1159
|
+
default:
|
|
1160
|
+
identity;
|
|
1161
|
+
}
|
|
1162
|
+
}
|
|
1163
|
+
return issues;
|
|
696
1164
|
}
|
|
697
|
-
function
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
}
|
|
1165
|
+
async function fixAssignees(tasks, registry, io) {
|
|
1166
|
+
const rewritten = [];
|
|
1167
|
+
for (const task of tasks) {
|
|
1168
|
+
if (task.assignee === void 0) continue;
|
|
1169
|
+
const identity = classifyAssignee(task.assignee, registry);
|
|
1170
|
+
if (identity.kind !== "correctable") continue;
|
|
1171
|
+
const content = await io.readFile(task.file);
|
|
1172
|
+
const i18n = getI18n(detectLocale(content));
|
|
1173
|
+
const label = readMetadataField(content, i18n.assignee) === void 0 ? "Assignee" : i18n.assignee;
|
|
1174
|
+
const next = writeMetadataField(content, label, identity.user.id);
|
|
1175
|
+
if (next !== content) {
|
|
1176
|
+
await io.writeFile(task.file, next);
|
|
1177
|
+
rewritten.push(task.file);
|
|
1178
|
+
}
|
|
1179
|
+
}
|
|
1180
|
+
return rewritten;
|
|
712
1181
|
}
|
|
713
|
-
function
|
|
714
|
-
|
|
1182
|
+
function isSeededUser(user) {
|
|
1183
|
+
const capitalized = user.id.charAt(0).toUpperCase() + user.id.slice(1);
|
|
1184
|
+
return user.email === `${user.id}@example.com` && user.name === capitalized;
|
|
715
1185
|
}
|
|
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 };
|
|
1186
|
+
function validateSeededUsers(assignees, registry) {
|
|
1187
|
+
const referenced = new Set(
|
|
1188
|
+
assignees.flatMap((raw) => {
|
|
1189
|
+
if (raw === void 0) return [];
|
|
1190
|
+
const identity = classifyAssignee(raw, registry);
|
|
1191
|
+
return identity.kind === "resolved" || identity.kind === "correctable" ? [identity.user.id] : [];
|
|
1192
|
+
})
|
|
1193
|
+
);
|
|
1194
|
+
return registry.getAllUsers().filter((user) => isSeededUser(user) && !referenced.has(user.id)).map((user) => ({
|
|
1195
|
+
file: `${".taskin/.taskin-users.json"} (${user.id})`,
|
|
1196
|
+
message: `User "${user.id}" looks like the placeholder that older taskin versions seeded on init (${user.email}), and no task points at it.`,
|
|
1197
|
+
severity: "warning",
|
|
1198
|
+
suggestion: `Remove it from the registry, or give it the real name and e-mail of whoever it stands for.`
|
|
1199
|
+
}));
|
|
743
1200
|
}
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
1201
|
+
var PLACEHOLDER_ASSIGNEES, foldAssignee, fold;
|
|
1202
|
+
var init_assignee_identity = __esm({
|
|
1203
|
+
"../file-system-task-provider/src/assignee-identity.ts"() {
|
|
1204
|
+
"use strict";
|
|
1205
|
+
init_esm_shims();
|
|
1206
|
+
init_i18n();
|
|
1207
|
+
init_metadata_style2();
|
|
1208
|
+
PLACEHOLDER_ASSIGNEES = [
|
|
1209
|
+
"a definir",
|
|
1210
|
+
"to be defined",
|
|
1211
|
+
"nome do responsavel",
|
|
1212
|
+
"nome do respons\xE1vel",
|
|
1213
|
+
"tbd",
|
|
1214
|
+
"-"
|
|
1215
|
+
];
|
|
1216
|
+
foldAssignee = (value) => value.toLowerCase().replace(/[^a-z0-9]/g, "");
|
|
1217
|
+
fold = foldAssignee;
|
|
1218
|
+
}
|
|
1219
|
+
});
|
|
1220
|
+
|
|
1221
|
+
// ../file-system-task-provider/src/auto-sync.ts
|
|
1222
|
+
function isNonFastForwardError(error2) {
|
|
1223
|
+
if (error2 instanceof Error) {
|
|
1224
|
+
return error2.message.toLowerCase().includes("non-fast-forward");
|
|
1225
|
+
}
|
|
1226
|
+
return false;
|
|
755
1227
|
}
|
|
756
|
-
async function
|
|
757
|
-
if (!
|
|
758
|
-
|
|
1228
|
+
async function syncBeforeCreate(git, config) {
|
|
1229
|
+
if (!config.autoSync || !config.defaultBranch) {
|
|
1230
|
+
if (config.autoSync && !config.defaultBranch) {
|
|
1231
|
+
console.warn("autoSync is enabled but no defaultBranch is configured. Nothing will be synced.");
|
|
1232
|
+
}
|
|
1233
|
+
return;
|
|
1234
|
+
}
|
|
1235
|
+
const fetchOk = await safeCall(() => git.fetch());
|
|
1236
|
+
if (!fetchOk) {
|
|
1237
|
+
throw new Error("Fetch failed. Check your network connection.");
|
|
1238
|
+
}
|
|
1239
|
+
const rebaseOk = await safeCall(() => git.rebase(`origin/${config.defaultBranch}`));
|
|
1240
|
+
if (!rebaseOk) {
|
|
1241
|
+
await git.abortRebase();
|
|
1242
|
+
throw new Error("Rebase failed due to conflict. Aborted.");
|
|
1243
|
+
}
|
|
1244
|
+
}
|
|
1245
|
+
async function safeCall(fn) {
|
|
1246
|
+
try {
|
|
1247
|
+
return await fn();
|
|
1248
|
+
} catch {
|
|
1249
|
+
return false;
|
|
1250
|
+
}
|
|
1251
|
+
}
|
|
1252
|
+
async function attemptPushWithRetry(git, pattern, message, branch, maxAttempts) {
|
|
1253
|
+
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
1254
|
+
if (attempt > 1) {
|
|
1255
|
+
const fetchOk = await safeCall(() => git.fetch());
|
|
1256
|
+
if (!fetchOk) {
|
|
1257
|
+
throw new Error("Fetch failed during retry.");
|
|
1258
|
+
}
|
|
1259
|
+
const rebaseOk = await safeCall(() => git.rebase(`origin/${branch}`));
|
|
1260
|
+
if (!rebaseOk) {
|
|
1261
|
+
throw new Error("Rebase failed during retry.");
|
|
1262
|
+
}
|
|
1263
|
+
}
|
|
1264
|
+
await git.addAndCommit(pattern, message);
|
|
1265
|
+
try {
|
|
1266
|
+
const pushOk = await git.push(branch);
|
|
1267
|
+
if (pushOk) {
|
|
1268
|
+
return;
|
|
1269
|
+
}
|
|
1270
|
+
} catch (error2) {
|
|
1271
|
+
if (!isNonFastForwardError(error2)) {
|
|
1272
|
+
throw error2;
|
|
1273
|
+
}
|
|
1274
|
+
if (attempt >= maxAttempts) {
|
|
1275
|
+
throw new Error(`Push rejected after ${maxAttempts} retries. Exhausted retry limit.`);
|
|
1276
|
+
}
|
|
1277
|
+
continue;
|
|
1278
|
+
}
|
|
1279
|
+
if (attempt >= maxAttempts) {
|
|
1280
|
+
throw new Error(`Push rejected after ${maxAttempts} retries. Exhausted retry limit.`);
|
|
1281
|
+
}
|
|
1282
|
+
}
|
|
1283
|
+
throw new Error(`Push rejected after ${maxAttempts} retries. Exhausted retry limit.`);
|
|
1284
|
+
}
|
|
1285
|
+
async function pushAfterCreate(git, options) {
|
|
1286
|
+
const pattern = `TASKS/task-${options.taskId}-*.md`;
|
|
1287
|
+
const message = appendCiSkipTag(`docs(TASKS): task-${options.taskId} - ${options.title}`, options.ciSkipTag);
|
|
1288
|
+
await attemptPushWithRetry(git, pattern, message, options.defaultBranch, MAX_RETRY_ATTEMPTS);
|
|
1289
|
+
return true;
|
|
1290
|
+
}
|
|
1291
|
+
async function getNextTaskNumberAfterSync(options) {
|
|
1292
|
+
const maxLocal = options.localCount;
|
|
1293
|
+
const maxRemote = options.remoteCount;
|
|
1294
|
+
if (options.autoSync) {
|
|
1295
|
+
const maxNumber = Math.max(maxLocal, maxRemote);
|
|
1296
|
+
return maxNumber + 1;
|
|
1297
|
+
}
|
|
1298
|
+
return maxLocal + 1;
|
|
1299
|
+
}
|
|
1300
|
+
async function createTaskWithSync(git, config, taskOptions) {
|
|
1301
|
+
await syncBeforeCreate(git, config);
|
|
1302
|
+
const nextNumber = await getNextTaskNumberAfterSync({
|
|
1303
|
+
autoSync: config.autoSync,
|
|
1304
|
+
localCount: 0,
|
|
1305
|
+
remoteCount: 0
|
|
1306
|
+
});
|
|
1307
|
+
const taskId = String(nextNumber).padStart(3, "0");
|
|
1308
|
+
if (config.autoSync && config.defaultBranch) {
|
|
1309
|
+
await pushAfterCreate(git, {
|
|
1310
|
+
taskId,
|
|
1311
|
+
title: taskOptions.title,
|
|
1312
|
+
defaultBranch: config.defaultBranch,
|
|
1313
|
+
ciSkipTag: config.ciSkipTag
|
|
1314
|
+
});
|
|
1315
|
+
}
|
|
1316
|
+
return { taskId };
|
|
1317
|
+
}
|
|
1318
|
+
async function squashTaskFileOnDone(git, options) {
|
|
1319
|
+
if (!options.originBranch) {
|
|
1320
|
+
return false;
|
|
1321
|
+
}
|
|
1322
|
+
const currentBranch = await git.getCurrentBranch();
|
|
1323
|
+
const originBranch = options.originBranch;
|
|
1324
|
+
const pattern = `TASKS/task-${options.taskId}-*.md`;
|
|
1325
|
+
const assetsPattern = `TASKS/assets/task-${options.taskId}/`;
|
|
1326
|
+
const coResult = await git.checkoutBranch(originBranch);
|
|
1327
|
+
if (!coResult) {
|
|
1328
|
+
await git.checkoutBranch(currentBranch);
|
|
1329
|
+
return false;
|
|
1330
|
+
}
|
|
1331
|
+
try {
|
|
1332
|
+
const patternsToAdd = [];
|
|
1333
|
+
const fileOk = await git.checkoutFile(options.defaultBranch, pattern);
|
|
1334
|
+
if (fileOk) {
|
|
1335
|
+
patternsToAdd.push(pattern);
|
|
1336
|
+
}
|
|
1337
|
+
const assetsOk = await git.checkoutFile(options.defaultBranch, assetsPattern);
|
|
1338
|
+
if (assetsOk) {
|
|
1339
|
+
patternsToAdd.push(assetsPattern);
|
|
1340
|
+
}
|
|
1341
|
+
if (patternsToAdd.length === 0) {
|
|
1342
|
+
await git.checkoutBranch(currentBranch);
|
|
1343
|
+
return false;
|
|
1344
|
+
}
|
|
1345
|
+
const combinedPattern = patternsToAdd.join(" ");
|
|
1346
|
+
const addOk = await git.addFiles(combinedPattern);
|
|
1347
|
+
if (!addOk) {
|
|
1348
|
+
await git.checkoutBranch(currentBranch);
|
|
1349
|
+
return false;
|
|
1350
|
+
}
|
|
1351
|
+
const message = appendCiSkipTag(`docs(TASKS): task-${options.taskId} - done`, options.ciSkipTag);
|
|
1352
|
+
const commitOk = await git.commit(message);
|
|
1353
|
+
if (!commitOk) {
|
|
1354
|
+
await git.checkoutBranch(currentBranch);
|
|
1355
|
+
return false;
|
|
1356
|
+
}
|
|
1357
|
+
for (let attempt = 1; attempt <= MAX_RETRY_ATTEMPTS; attempt++) {
|
|
1358
|
+
if (attempt > 1) {
|
|
1359
|
+
await safeCall(() => git.fetch());
|
|
1360
|
+
await safeCall(() => git.rebase(`origin/${originBranch}`));
|
|
1361
|
+
}
|
|
1362
|
+
const pushOk = await safeCall(() => git.push(originBranch));
|
|
1363
|
+
if (pushOk === true) {
|
|
1364
|
+
await git.checkoutBranch(currentBranch);
|
|
1365
|
+
return true;
|
|
1366
|
+
}
|
|
1367
|
+
if (attempt >= MAX_RETRY_ATTEMPTS) {
|
|
1368
|
+
await git.checkoutBranch(currentBranch);
|
|
1369
|
+
throw new Error(`Squash push rejected after ${MAX_RETRY_ATTEMPTS} retries.`);
|
|
1370
|
+
}
|
|
1371
|
+
}
|
|
1372
|
+
await git.checkoutBranch(currentBranch);
|
|
1373
|
+
throw new Error(`Squash push rejected after ${MAX_RETRY_ATTEMPTS} retries.`);
|
|
1374
|
+
} catch (error2) {
|
|
1375
|
+
await git.checkoutBranch(currentBranch);
|
|
1376
|
+
if (error2 instanceof Error) {
|
|
1377
|
+
throw error2;
|
|
1378
|
+
}
|
|
1379
|
+
return false;
|
|
1380
|
+
}
|
|
1381
|
+
}
|
|
1382
|
+
var MAX_RETRY_ATTEMPTS;
|
|
1383
|
+
var init_auto_sync = __esm({
|
|
1384
|
+
"../file-system-task-provider/src/auto-sync.ts"() {
|
|
1385
|
+
"use strict";
|
|
1386
|
+
init_esm_shims();
|
|
1387
|
+
init_src();
|
|
1388
|
+
MAX_RETRY_ATTEMPTS = 3;
|
|
1389
|
+
}
|
|
1390
|
+
});
|
|
1391
|
+
|
|
1392
|
+
// ../file-system-task-provider/src/file-system-metrics-adapter.ts
|
|
1393
|
+
import {
|
|
1394
|
+
TASK_STATUSES,
|
|
1395
|
+
TASK_TYPES,
|
|
1396
|
+
UserStatsSchema
|
|
1397
|
+
} from "@opentask/taskin-types";
|
|
1398
|
+
import { promises as fs } from "fs";
|
|
1399
|
+
import path3 from "path";
|
|
1400
|
+
function toISOString(date) {
|
|
1401
|
+
return date.toISOString();
|
|
1402
|
+
}
|
|
1403
|
+
function emptyCodeMetrics() {
|
|
1404
|
+
return {
|
|
1405
|
+
linesAdded: 0,
|
|
1406
|
+
linesRemoved: 0,
|
|
1407
|
+
netChange: 0,
|
|
1408
|
+
characters: 0,
|
|
1409
|
+
filesChanged: 0,
|
|
1410
|
+
commits: 0
|
|
1411
|
+
};
|
|
1412
|
+
}
|
|
1413
|
+
function emptyTemporalMetrics() {
|
|
1414
|
+
return {
|
|
1415
|
+
byDayOfWeek: {
|
|
1416
|
+
"0": 0,
|
|
1417
|
+
"1": 0,
|
|
1418
|
+
"2": 0,
|
|
1419
|
+
"3": 0,
|
|
1420
|
+
"4": 0,
|
|
1421
|
+
"5": 0,
|
|
1422
|
+
"6": 0
|
|
1423
|
+
},
|
|
1424
|
+
byTimeOfDay: { morning: 0, afternoon: 0, evening: 0, night: 0 },
|
|
1425
|
+
streak: 0,
|
|
1426
|
+
trend: "stable"
|
|
1427
|
+
};
|
|
1428
|
+
}
|
|
1429
|
+
function removeCodeBlocks(content) {
|
|
1430
|
+
return content.replace(/```[\s\S]*?```/g, "");
|
|
1431
|
+
}
|
|
1432
|
+
function resolvePeriod(period = "week") {
|
|
1433
|
+
const now = /* @__PURE__ */ new Date();
|
|
1434
|
+
const until = now;
|
|
1435
|
+
let since;
|
|
1436
|
+
switch (period) {
|
|
1437
|
+
case "day":
|
|
1438
|
+
since = new Date(Date.now() - MILLISECONDS_PER_DAY);
|
|
1439
|
+
break;
|
|
1440
|
+
case "week":
|
|
1441
|
+
since = new Date(Date.now() - DAYS_PER_WEEK * MILLISECONDS_PER_DAY);
|
|
1442
|
+
break;
|
|
1443
|
+
case "month":
|
|
1444
|
+
since = new Date(Date.now() - 30 * MILLISECONDS_PER_DAY);
|
|
1445
|
+
break;
|
|
1446
|
+
case "quarter":
|
|
1447
|
+
since = new Date(Date.now() - 90 * MILLISECONDS_PER_DAY);
|
|
1448
|
+
break;
|
|
1449
|
+
case "year":
|
|
1450
|
+
since = new Date(Date.now() - 365 * MILLISECONDS_PER_DAY);
|
|
1451
|
+
break;
|
|
1452
|
+
case "all":
|
|
1453
|
+
since = /* @__PURE__ */ new Date(0);
|
|
1454
|
+
break;
|
|
1455
|
+
default:
|
|
1456
|
+
since = new Date(Date.now() - DAYS_PER_WEEK * MILLISECONDS_PER_DAY);
|
|
1457
|
+
}
|
|
1458
|
+
return { since, until };
|
|
1459
|
+
}
|
|
1460
|
+
function calculateActivityFrequency(commits, period) {
|
|
1461
|
+
const daysInPeriod = {
|
|
1462
|
+
day: 1,
|
|
1463
|
+
week: 7,
|
|
1464
|
+
month: 30,
|
|
1465
|
+
quarter: 90,
|
|
1466
|
+
year: 365,
|
|
1467
|
+
all: 365
|
|
1468
|
+
// Use 1 year as baseline for 'all'
|
|
1469
|
+
};
|
|
1470
|
+
return commits / daysInPeriod[period];
|
|
1471
|
+
}
|
|
1472
|
+
async function calculateCodeMetrics(gitAnalyzer, username, since, until) {
|
|
1473
|
+
if (!gitAnalyzer) {
|
|
1474
|
+
return emptyCodeMetrics();
|
|
759
1475
|
}
|
|
760
1476
|
try {
|
|
761
1477
|
const commits = await gitAnalyzer.getCommits({
|
|
@@ -1116,56 +1832,6 @@ var init_file_system_metrics_adapter = __esm({
|
|
|
1116
1832
|
}
|
|
1117
1833
|
});
|
|
1118
1834
|
|
|
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
1835
|
// ../file-system-task-provider/src/task-validator.ts
|
|
1170
1836
|
var task_validator_exports = {};
|
|
1171
1837
|
__export(task_validator_exports, {
|
|
@@ -1343,7 +2009,7 @@ var init_task_validator = __esm({
|
|
|
1343
2009
|
});
|
|
1344
2010
|
|
|
1345
2011
|
// ../file-system-task-provider/src/user-registry.ts
|
|
1346
|
-
import { execSync } from "child_process";
|
|
2012
|
+
import { execSync as execSync3 } from "child_process";
|
|
1347
2013
|
import { createHash } from "crypto";
|
|
1348
2014
|
import { promises as fs2 } from "fs";
|
|
1349
2015
|
import path4 from "path";
|
|
@@ -1460,7 +2126,7 @@ var init_user_registry = __esm({
|
|
|
1460
2126
|
}
|
|
1461
2127
|
readGitConfig(key) {
|
|
1462
2128
|
try {
|
|
1463
|
-
return
|
|
2129
|
+
return execSync3(`git config ${key}`, {
|
|
1464
2130
|
encoding: "utf-8",
|
|
1465
2131
|
stdio: "pipe"
|
|
1466
2132
|
}).trim();
|
|
@@ -1636,7 +2302,7 @@ var init_file_system_task_provider = __esm({
|
|
|
1636
2302
|
"../file-system-task-provider/src/file-system-task-provider.ts"() {
|
|
1637
2303
|
"use strict";
|
|
1638
2304
|
init_esm_shims();
|
|
1639
|
-
|
|
2305
|
+
init_src2();
|
|
1640
2306
|
init_assignee_identity();
|
|
1641
2307
|
init_i18n();
|
|
1642
2308
|
init_metadata_style2();
|
|
@@ -2045,7 +2711,7 @@ __export(src_exports, {
|
|
|
2045
2711
|
validateUsersFileLocation: () => validateUsersFileLocation,
|
|
2046
2712
|
writeMetadataField: () => writeMetadataField
|
|
2047
2713
|
});
|
|
2048
|
-
var
|
|
2714
|
+
var init_src3 = __esm({
|
|
2049
2715
|
"../file-system-task-provider/src/index.ts"() {
|
|
2050
2716
|
"use strict";
|
|
2051
2717
|
init_esm_shims();
|
|
@@ -2068,6 +2734,7 @@ import { Command } from "commander";
|
|
|
2068
2734
|
|
|
2069
2735
|
// src/commands/config.ts
|
|
2070
2736
|
init_esm_shims();
|
|
2737
|
+
init_src();
|
|
2071
2738
|
import chalk2 from "chalk";
|
|
2072
2739
|
import inquirer from "inquirer";
|
|
2073
2740
|
|
|
@@ -2114,9 +2781,15 @@ function warning(message) {
|
|
|
2114
2781
|
|
|
2115
2782
|
// src/lib/config-manager.ts
|
|
2116
2783
|
init_esm_shims();
|
|
2784
|
+
init_src();
|
|
2117
2785
|
import { TaskinConfigSchema } from "@opentask/taskin-types";
|
|
2118
2786
|
import { existsSync, readFileSync, writeFileSync } from "fs";
|
|
2119
2787
|
import { join } from "path";
|
|
2788
|
+
var DEFAULT_AUTOMATION_CONFIG = {
|
|
2789
|
+
level: "assisted",
|
|
2790
|
+
autoSync: true,
|
|
2791
|
+
ciSkipTag: DEFAULT_CI_SKIP_TAG
|
|
2792
|
+
};
|
|
2120
2793
|
function getAutomationBehavior(level, commits) {
|
|
2121
2794
|
const presets = {
|
|
2122
2795
|
manual: {
|
|
@@ -2170,6 +2843,9 @@ ${result.error.message}`);
|
|
|
2170
2843
|
}
|
|
2171
2844
|
/**
|
|
2172
2845
|
* Save configuration to .taskin.json
|
|
2846
|
+
*
|
|
2847
|
+
* Takes the *input* shape: the schema fills in what it has defaults for, so
|
|
2848
|
+
* a caller need not spell out `automation.ciSkipTag` to save a config.
|
|
2173
2849
|
*/
|
|
2174
2850
|
saveConfig(config) {
|
|
2175
2851
|
const validated = TaskinConfigSchema.parse(config);
|
|
@@ -2193,7 +2869,7 @@ ${result.error.message}`);
|
|
|
2193
2869
|
setAutomationLevel(level) {
|
|
2194
2870
|
const config = this.loadConfig();
|
|
2195
2871
|
config.automation = {
|
|
2196
|
-
|
|
2872
|
+
...DEFAULT_AUTOMATION_CONFIG,
|
|
2197
2873
|
...config.automation,
|
|
2198
2874
|
level
|
|
2199
2875
|
};
|
|
@@ -2205,11 +2881,33 @@ ${result.error.message}`);
|
|
|
2205
2881
|
getAutomationConfig() {
|
|
2206
2882
|
try {
|
|
2207
2883
|
const config = this.loadConfig();
|
|
2208
|
-
return config.automation ??
|
|
2884
|
+
return config.automation ?? DEFAULT_AUTOMATION_CONFIG;
|
|
2209
2885
|
} catch {
|
|
2210
|
-
return
|
|
2886
|
+
return DEFAULT_AUTOMATION_CONFIG;
|
|
2211
2887
|
}
|
|
2212
2888
|
}
|
|
2889
|
+
/**
|
|
2890
|
+
* Tag appended to the commits Taskin writes on its own, so a status change
|
|
2891
|
+
* does not trigger the project's pipeline.
|
|
2892
|
+
*
|
|
2893
|
+
* Returns `[skip ci]` when unconfigured. An empty string is a real answer —
|
|
2894
|
+
* it means the project wants CI to run — so it is returned as-is.
|
|
2895
|
+
*/
|
|
2896
|
+
getCiSkipTag() {
|
|
2897
|
+
return this.getAutomationConfig().ciSkipTag ?? DEFAULT_CI_SKIP_TAG;
|
|
2898
|
+
}
|
|
2899
|
+
/**
|
|
2900
|
+
* Set the CI-skip tag, preserving the rest of the automation block.
|
|
2901
|
+
*/
|
|
2902
|
+
setCiSkipTag(ciSkipTag) {
|
|
2903
|
+
const config = this.loadConfig();
|
|
2904
|
+
config.automation = {
|
|
2905
|
+
...DEFAULT_AUTOMATION_CONFIG,
|
|
2906
|
+
...config.automation,
|
|
2907
|
+
ciSkipTag
|
|
2908
|
+
};
|
|
2909
|
+
this.saveConfig(config);
|
|
2910
|
+
}
|
|
2213
2911
|
/**
|
|
2214
2912
|
* Set automation configuration
|
|
2215
2913
|
*/
|
|
@@ -2227,7 +2925,8 @@ ${result.error.message}`);
|
|
|
2227
2925
|
...getAutomationBehavior(automation.level, automation.commits),
|
|
2228
2926
|
defaultBranch: automation.defaultBranch,
|
|
2229
2927
|
autoSync: automation.autoSync,
|
|
2230
|
-
originBranch: automation.originBranch
|
|
2928
|
+
originBranch: automation.originBranch,
|
|
2929
|
+
ciSkipTag: automation.ciSkipTag ?? DEFAULT_CI_SKIP_TAG
|
|
2231
2930
|
};
|
|
2232
2931
|
}
|
|
2233
2932
|
/**
|
|
@@ -2339,6 +3038,14 @@ var defineCommand = (config) => {
|
|
|
2339
3038
|
init_esm_shims();
|
|
2340
3039
|
|
|
2341
3040
|
// src/commands/config.ts
|
|
3041
|
+
var NO_CI_SKIP_TAG_KEYWORD = "none";
|
|
3042
|
+
function normalizeCiSkipTagInput(input) {
|
|
3043
|
+
const trimmed = input.trim();
|
|
3044
|
+
return trimmed.toLowerCase() === NO_CI_SKIP_TAG_KEYWORD ? "" : trimmed;
|
|
3045
|
+
}
|
|
3046
|
+
function describeCiSkipTag(tag) {
|
|
3047
|
+
return tag.length === 0 ? "no tag \u2014 CI runs on status commits" : tag;
|
|
3048
|
+
}
|
|
2342
3049
|
var configCommand = defineCommand({
|
|
2343
3050
|
name: "config",
|
|
2344
3051
|
description: "\u2699\uFE0F Configure Taskin settings",
|
|
@@ -2358,6 +3065,10 @@ var configCommand = defineCommand({
|
|
|
2358
3065
|
{
|
|
2359
3066
|
flags: "--notification-events <events>",
|
|
2360
3067
|
description: "Comma-separated events (task:start,task:done,task:review)"
|
|
3068
|
+
},
|
|
3069
|
+
{
|
|
3070
|
+
flags: "--ci-skip-tag <tag>",
|
|
3071
|
+
description: `Tag appended to Taskin's own commits so they skip CI (default "${DEFAULT_CI_SKIP_TAG}"; "none" to run CI)`
|
|
2361
3072
|
}
|
|
2362
3073
|
],
|
|
2363
3074
|
handler: async (options) => {
|
|
@@ -2375,8 +3086,12 @@ async function handleConfigCommand(options) {
|
|
|
2375
3086
|
await setAutomationLevel(configManager, options.level);
|
|
2376
3087
|
return;
|
|
2377
3088
|
}
|
|
2378
|
-
if (options
|
|
2379
|
-
|
|
3089
|
+
if (options.ciSkipTag !== void 0) {
|
|
3090
|
+
setCiSkipTag(configManager, options.ciSkipTag);
|
|
3091
|
+
return;
|
|
3092
|
+
}
|
|
3093
|
+
if (options.discordWebhook) {
|
|
3094
|
+
await setDiscordNotification(configManager, options.discordWebhook, options.notificationEvents);
|
|
2380
3095
|
return;
|
|
2381
3096
|
}
|
|
2382
3097
|
await interactiveConfig(configManager);
|
|
@@ -2397,7 +3112,8 @@ async function showConfiguration(configManager) {
|
|
|
2397
3112
|
` Auto-commit status changes: ${behavior.autoCommitStatusChange ? chalk2.green("\u2713 Yes") : chalk2.red("\u2717 No")}`
|
|
2398
3113
|
);
|
|
2399
3114
|
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")}
|
|
3115
|
+
console.log(` Auto-commit on finish: ${behavior.autoCommitFinish ? chalk2.green("\u2713 Yes") : chalk2.red("\u2717 No")}`);
|
|
3116
|
+
console.log(` CI skip tag: ${chalk2.cyan(describeCiSkipTag(configManager.getCiSkipTag()))}
|
|
2401
3117
|
`);
|
|
2402
3118
|
console.log(chalk2.bold("\u{1F514} Notifications"));
|
|
2403
3119
|
const notifications = configManager.getNotifications();
|
|
@@ -2432,6 +3148,24 @@ async function showConfiguration(configManager) {
|
|
|
2432
3148
|
process.exit(1);
|
|
2433
3149
|
}
|
|
2434
3150
|
}
|
|
3151
|
+
function warnAboutUnrecognizedCiSkipTag(tag) {
|
|
3152
|
+
if (tag.length === 0 || isRecognizedCiSkipTag(tag)) return;
|
|
3153
|
+
console.log();
|
|
3154
|
+
console.log(chalk2.yellow("\u26A0\uFE0F This tag is not one GitHub, GitLab or Bitbucket documents."));
|
|
3155
|
+
if (tag.replace(/\s+/g, "").toLowerCase() === "[skip-ci]") {
|
|
3156
|
+
console.log(chalk2.dim(' "[skip-ci]" with a hyphen is not recognized anywhere \u2014 it triggers CI.'));
|
|
3157
|
+
console.log(chalk2.dim(' Did you mean "[skip ci]", with a space?'));
|
|
3158
|
+
}
|
|
3159
|
+
console.log(chalk2.dim(` Documented tags: ${CI_SKIP_TAGS.join(", ")}`));
|
|
3160
|
+
console.log(chalk2.dim(" Keeping it anyway \u2014 a self-hosted pipeline can match whatever it likes."));
|
|
3161
|
+
}
|
|
3162
|
+
function setCiSkipTag(configManager, rawTag) {
|
|
3163
|
+
printHeader("Configure CI Skip Tag", "\u2699\uFE0F");
|
|
3164
|
+
const tag = normalizeCiSkipTagInput(rawTag);
|
|
3165
|
+
configManager.setCiSkipTag(tag);
|
|
3166
|
+
success(`CI skip tag set to ${colors.highlight(describeCiSkipTag(tag))}`);
|
|
3167
|
+
warnAboutUnrecognizedCiSkipTag(tag);
|
|
3168
|
+
}
|
|
2435
3169
|
async function setDiscordNotification(configManager, webhookUrl, eventsFlag) {
|
|
2436
3170
|
printHeader("Configure Discord Notification", "\u{1F514}");
|
|
2437
3171
|
const events = eventsFlag ? eventsFlag.split(",").map((e) => e.trim()) : ["task:start", "task:done"];
|
|
@@ -2487,6 +3221,7 @@ async function interactiveConfig(configManager) {
|
|
|
2487
3221
|
message: "What would you like to configure?",
|
|
2488
3222
|
choices: [
|
|
2489
3223
|
{ name: "\u{1F916} Automation level", value: "automation" },
|
|
3224
|
+
{ name: "\u23ED\uFE0F CI skip tag", value: "ciSkipTag" },
|
|
2490
3225
|
{ name: "\u{1F514} Discord notification", value: "discord" },
|
|
2491
3226
|
{ name: "\u{1F514} Telegram notification", value: "telegram" }
|
|
2492
3227
|
]
|
|
@@ -2494,6 +3229,8 @@ async function interactiveConfig(configManager) {
|
|
|
2494
3229
|
]);
|
|
2495
3230
|
if (section === "automation") {
|
|
2496
3231
|
await configureAutomation(configManager);
|
|
3232
|
+
} else if (section === "ciSkipTag") {
|
|
3233
|
+
await configureCiSkipTag(configManager);
|
|
2497
3234
|
} else if (section === "discord") {
|
|
2498
3235
|
await configureDiscordNotification(configManager);
|
|
2499
3236
|
} else if (section === "telegram") {
|
|
@@ -2545,6 +3282,40 @@ async function configureAutomation(configManager) {
|
|
|
2545
3282
|
console.log(chalk2.dim(` Auto-commit on pause: ${behavior.autoCommitPause ? "\u2713" : "\u2717"}`));
|
|
2546
3283
|
console.log(chalk2.dim(` Auto-commit on finish: ${behavior.autoCommitFinish ? "\u2713" : "\u2717"}`));
|
|
2547
3284
|
}
|
|
3285
|
+
async function configureCiSkipTag(configManager) {
|
|
3286
|
+
printHeader("Configure CI Skip Tag", "\u23ED\uFE0F");
|
|
3287
|
+
const current = configManager.getCiSkipTag();
|
|
3288
|
+
console.log(`Current tag: ${chalk2.cyan(describeCiSkipTag(current))}
|
|
3289
|
+
`);
|
|
3290
|
+
console.log(chalk2.dim("Taskin appends this to the commits it writes itself \u2014 status changes and"));
|
|
3291
|
+
console.log(chalk2.dim("task files \u2014 so they do not trigger your pipeline.\n"));
|
|
3292
|
+
const { choice } = await inquirer.prompt([
|
|
3293
|
+
{
|
|
3294
|
+
type: "list",
|
|
3295
|
+
name: "choice",
|
|
3296
|
+
message: "Tag for Taskin commits:",
|
|
3297
|
+
default: current.length === 0 ? NO_CI_SKIP_TAG_KEYWORD : current,
|
|
3298
|
+
choices: [
|
|
3299
|
+
{ name: `${CI_SKIP_TAGS[0]} \u2014 GitHub, GitLab and Bitbucket (recommended)`, value: CI_SKIP_TAGS[0] },
|
|
3300
|
+
{ name: `${CI_SKIP_TAGS[1]} \u2014 GitHub, GitLab and Bitbucket`, value: CI_SKIP_TAGS[1] },
|
|
3301
|
+
{ name: `${CI_SKIP_TAGS[2]} \u2014 GitHub Actions only`, value: CI_SKIP_TAGS[2] },
|
|
3302
|
+
{ name: `${CI_SKIP_TAGS[3]} \u2014 GitHub Actions only`, value: CI_SKIP_TAGS[3] },
|
|
3303
|
+
{ name: `${CI_SKIP_TAGS[4]} \u2014 GitHub Actions only`, value: CI_SKIP_TAGS[4] },
|
|
3304
|
+
{ name: "none \u2014 do not mark the commits, let CI run", value: NO_CI_SKIP_TAG_KEYWORD },
|
|
3305
|
+
{ name: "custom\u2026 \u2014 another CI (Azure DevOps uses ***NO_CI***)", value: "custom" }
|
|
3306
|
+
]
|
|
3307
|
+
}
|
|
3308
|
+
]);
|
|
3309
|
+
const tag = choice === "custom" ? (await inquirer.prompt([
|
|
3310
|
+
{
|
|
3311
|
+
type: "input",
|
|
3312
|
+
name: "customTag",
|
|
3313
|
+
message: "Tag to append:",
|
|
3314
|
+
default: current
|
|
3315
|
+
}
|
|
3316
|
+
])).customTag : choice;
|
|
3317
|
+
setCiSkipTag(configManager, tag);
|
|
3318
|
+
}
|
|
2548
3319
|
async function configureDiscordNotification(configManager) {
|
|
2549
3320
|
printHeader("Configure Discord Notification", "\u{1F514}");
|
|
2550
3321
|
const notifications = configManager.getNotifications() ?? {};
|
|
@@ -3000,1220 +3771,640 @@ var TaskWebSocketServer = class {
|
|
|
3000
3771
|
await this.handlePauseRequest(client, message);
|
|
3001
3772
|
break;
|
|
3002
3773
|
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}...`);
|
|
3774
|
+
this.sendToClient(client.id, { type: "pong" });
|
|
3775
|
+
break;
|
|
3776
|
+
default:
|
|
3777
|
+
this.sendToClient(client.id, {
|
|
3778
|
+
type: "error",
|
|
3779
|
+
payload: { message: `Unknown message type: ${message.type}` }
|
|
3780
|
+
});
|
|
3429
3781
|
}
|
|
3782
|
+
} catch (error2) {
|
|
3783
|
+
this.log("Error handling message:", error2);
|
|
3784
|
+
this.sendToClient(client.id, {
|
|
3785
|
+
type: "error",
|
|
3786
|
+
payload: {
|
|
3787
|
+
message: error2 instanceof Error ? error2.message : "Failed to process message"
|
|
3788
|
+
}
|
|
3789
|
+
});
|
|
3430
3790
|
}
|
|
3431
3791
|
}
|
|
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)"
|
|
3792
|
+
/**
|
|
3793
|
+
* Reads the task id out of an incoming message.
|
|
3794
|
+
*
|
|
3795
|
+
* Ids on the wire are untrusted strings: a branded `TaskId` is only worth
|
|
3796
|
+
* something if the boundary that produces it actually validates. Answers the
|
|
3797
|
+
* client with a clear error instead of letting a ZodError leak out of the
|
|
3798
|
+
* generic catch.
|
|
3799
|
+
*/
|
|
3800
|
+
readTaskId(client, message, field = "taskId") {
|
|
3801
|
+
const raw = message.payload?.[field];
|
|
3802
|
+
const parsed = typeof raw === "string" ? TaskIdSchema.safeParse(raw) : void 0;
|
|
3803
|
+
if (!parsed?.success) {
|
|
3804
|
+
this.sendToClient(client.id, {
|
|
3805
|
+
type: "error",
|
|
3806
|
+
payload: { message: `Invalid task id in '${message.type}' request` },
|
|
3807
|
+
requestId: message.requestId
|
|
3808
|
+
});
|
|
3809
|
+
return void 0;
|
|
3467
3810
|
}
|
|
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);
|
|
3811
|
+
return parsed.data;
|
|
3490
3812
|
}
|
|
3491
|
-
|
|
3492
|
-
|
|
3493
|
-
|
|
3494
|
-
|
|
3495
|
-
|
|
3496
|
-
|
|
3813
|
+
/**
|
|
3814
|
+
* Handle list request
|
|
3815
|
+
*/
|
|
3816
|
+
async handleListRequest(client, message) {
|
|
3817
|
+
const tasks = await this.taskProvider.getAllTasks();
|
|
3818
|
+
const [first] = tasks;
|
|
3819
|
+
if (first) {
|
|
3820
|
+
this.log("[WS Server] Sending", tasks.length, "tasks");
|
|
3821
|
+
this.log("[WS Server] First task assignee:", first.assignee);
|
|
3822
|
+
}
|
|
3823
|
+
this.sendToClient(client.id, {
|
|
3824
|
+
type: "tasks",
|
|
3825
|
+
payload: tasks,
|
|
3826
|
+
requestId: message.requestId
|
|
3827
|
+
});
|
|
3497
3828
|
}
|
|
3498
|
-
|
|
3499
|
-
|
|
3500
|
-
|
|
3501
|
-
|
|
3829
|
+
/**
|
|
3830
|
+
* Handle find request
|
|
3831
|
+
*/
|
|
3832
|
+
async handleFindRequest(client, message) {
|
|
3833
|
+
const taskId = this.readTaskId(client, message);
|
|
3834
|
+
if (!taskId)
|
|
3835
|
+
return;
|
|
3836
|
+
const task = await this.taskProvider.findTask(taskId);
|
|
3837
|
+
this.sendToClient(client.id, {
|
|
3838
|
+
type: "task:found",
|
|
3839
|
+
payload: task,
|
|
3840
|
+
requestId: message.requestId
|
|
3841
|
+
});
|
|
3502
3842
|
}
|
|
3503
|
-
|
|
3504
|
-
|
|
3505
|
-
|
|
3506
|
-
|
|
3507
|
-
|
|
3508
|
-
|
|
3509
|
-
|
|
3510
|
-
|
|
3511
|
-
|
|
3512
|
-
|
|
3513
|
-
|
|
3514
|
-
|
|
3515
|
-
|
|
3516
|
-
|
|
3517
|
-
|
|
3518
|
-
currentDir = path8.dirname(currentDir);
|
|
3519
|
-
}
|
|
3520
|
-
}
|
|
3843
|
+
/**
|
|
3844
|
+
* Handle update request
|
|
3845
|
+
*/
|
|
3846
|
+
async handleUpdateRequest(client, message) {
|
|
3847
|
+
const taskId = this.readTaskId(client, message, "id");
|
|
3848
|
+
if (!taskId)
|
|
3849
|
+
return;
|
|
3850
|
+
const stored = await this.taskProvider.findTask(taskId);
|
|
3851
|
+
if (!stored) {
|
|
3852
|
+
this.sendToClient(client.id, {
|
|
3853
|
+
type: "error",
|
|
3854
|
+
payload: { message: `Task ${taskId} not found` },
|
|
3855
|
+
requestId: message.requestId
|
|
3856
|
+
});
|
|
3857
|
+
return;
|
|
3521
3858
|
}
|
|
3522
|
-
|
|
3523
|
-
|
|
3524
|
-
|
|
3525
|
-
|
|
3526
|
-
|
|
3527
|
-
|
|
3528
|
-
|
|
3529
|
-
|
|
3530
|
-
|
|
3531
|
-
|
|
3532
|
-
|
|
3533
|
-
|
|
3859
|
+
const outcome = applyTaskUpdate(stored, message.payload);
|
|
3860
|
+
if (!outcome.ok) {
|
|
3861
|
+
this.sendToClient(client.id, {
|
|
3862
|
+
type: "error",
|
|
3863
|
+
payload: { message: outcome.message },
|
|
3864
|
+
requestId: message.requestId
|
|
3865
|
+
});
|
|
3866
|
+
return;
|
|
3867
|
+
}
|
|
3868
|
+
await this.taskProvider.updateTask(outcome.task);
|
|
3869
|
+
this.broadcast({
|
|
3870
|
+
type: "task:updated",
|
|
3871
|
+
payload: outcome.task
|
|
3534
3872
|
});
|
|
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();
|
|
3873
|
+
}
|
|
3874
|
+
/**
|
|
3875
|
+
* Handle start task request
|
|
3876
|
+
*/
|
|
3877
|
+
async handleStartRequest(client, message) {
|
|
3878
|
+
const taskId = this.readTaskId(client, message);
|
|
3879
|
+
if (!taskId)
|
|
3880
|
+
return;
|
|
3881
|
+
const task = await this.taskManager.startTask(taskId);
|
|
3882
|
+
this.broadcast({
|
|
3883
|
+
type: "task:updated",
|
|
3884
|
+
payload: task
|
|
3551
3885
|
});
|
|
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
|
-
}
|
|
3886
|
+
}
|
|
3887
|
+
/**
|
|
3888
|
+
* Handle finish task request
|
|
3889
|
+
*/
|
|
3890
|
+
async handleFinishRequest(client, message) {
|
|
3891
|
+
const taskId = this.readTaskId(client, message);
|
|
3892
|
+
if (!taskId)
|
|
3893
|
+
return;
|
|
3894
|
+
const task = await this.taskManager.finishTask(taskId);
|
|
3895
|
+
this.broadcast({
|
|
3896
|
+
type: "task:updated",
|
|
3897
|
+
payload: task
|
|
3569
3898
|
});
|
|
3570
|
-
|
|
3571
|
-
|
|
3572
|
-
|
|
3573
|
-
|
|
3574
|
-
|
|
3575
|
-
|
|
3576
|
-
|
|
3577
|
-
|
|
3578
|
-
|
|
3579
|
-
|
|
3580
|
-
|
|
3581
|
-
|
|
3899
|
+
}
|
|
3900
|
+
/**
|
|
3901
|
+
* Handle pause task request
|
|
3902
|
+
*/
|
|
3903
|
+
async handlePauseRequest(client, message) {
|
|
3904
|
+
const taskId = this.readTaskId(client, message);
|
|
3905
|
+
if (!taskId)
|
|
3906
|
+
return;
|
|
3907
|
+
const task = await this.taskManager.pauseTask(taskId);
|
|
3908
|
+
this.broadcast({
|
|
3909
|
+
type: "task:updated",
|
|
3910
|
+
payload: task
|
|
3582
3911
|
});
|
|
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}`);
|
|
3912
|
+
}
|
|
3913
|
+
/**
|
|
3914
|
+
* Start heartbeat to check client connections
|
|
3915
|
+
*/
|
|
3916
|
+
startHeartbeat() {
|
|
3917
|
+
this.heartbeatInterval = setInterval(() => {
|
|
3918
|
+
this.clients.forEach((client) => {
|
|
3919
|
+
if (!client.isAlive) {
|
|
3920
|
+
this.log(`Client ${client.id} timeout, terminating`);
|
|
3921
|
+
client.ws.terminate();
|
|
3922
|
+
this.clients.delete(client.id);
|
|
3923
|
+
return;
|
|
3924
|
+
}
|
|
3925
|
+
client.isAlive = false;
|
|
3926
|
+
client.ws.ping();
|
|
3600
3927
|
});
|
|
3928
|
+
}, this.options.heartbeatInterval);
|
|
3929
|
+
}
|
|
3930
|
+
/**
|
|
3931
|
+
* Stop heartbeat
|
|
3932
|
+
*/
|
|
3933
|
+
stopHeartbeat() {
|
|
3934
|
+
if (this.heartbeatInterval) {
|
|
3935
|
+
clearInterval(this.heartbeatInterval);
|
|
3936
|
+
this.heartbeatInterval = null;
|
|
3601
3937
|
}
|
|
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);
|
|
3938
|
+
}
|
|
3939
|
+
/**
|
|
3940
|
+
* Debug logging
|
|
3941
|
+
*/
|
|
3942
|
+
log(...args) {
|
|
3943
|
+
if (this.options.debug) {
|
|
3944
|
+
console.log("[TaskWebSocketServer]", ...args);
|
|
3628
3945
|
}
|
|
3629
|
-
process.exit(1);
|
|
3630
3946
|
}
|
|
3631
|
-
}
|
|
3947
|
+
};
|
|
3632
3948
|
|
|
3633
|
-
//
|
|
3949
|
+
// node_modules/@opentask/taskin-task-server-ws/dist/task-server-ws.mock.js
|
|
3950
|
+
init_esm_shims();
|
|
3951
|
+
import { parseTaskId } from "@opentask/taskin-types";
|
|
3952
|
+
|
|
3953
|
+
// node_modules/@opentask/taskin-task-server-ws/dist/task-server-ws.types.js
|
|
3634
3954
|
init_esm_shims();
|
|
3955
|
+
|
|
3956
|
+
// src/commands/dashboard.ts
|
|
3635
3957
|
init_src2();
|
|
3958
|
+
import chalk4 from "chalk";
|
|
3959
|
+
import express from "express";
|
|
3960
|
+
import { createServer } from "http";
|
|
3961
|
+
import path8 from "path";
|
|
3962
|
+
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
3636
3963
|
|
|
3637
|
-
//
|
|
3964
|
+
// src/lib/provider-factory/index.ts
|
|
3638
3965
|
init_esm_shims();
|
|
3639
3966
|
|
|
3640
|
-
//
|
|
3967
|
+
// src/lib/provider-factory/provider-factory.ts
|
|
3641
3968
|
init_esm_shims();
|
|
3642
|
-
import
|
|
3643
|
-
|
|
3969
|
+
import path7 from "path";
|
|
3970
|
+
|
|
3971
|
+
// src/lib/notification/env-resolver.ts
|
|
3972
|
+
init_esm_shims();
|
|
3973
|
+
import { readFileSync as readFileSync2 } from "fs";
|
|
3974
|
+
import { join as join2 } from "path";
|
|
3975
|
+
var ENV_VAR_PATTERN = /\$\{([^}]+)\}/g;
|
|
3976
|
+
function loadDotEnv(dir) {
|
|
3644
3977
|
try {
|
|
3645
|
-
|
|
3646
|
-
|
|
3647
|
-
|
|
3648
|
-
|
|
3978
|
+
const envPath = join2(dir ?? process.cwd(), ".env");
|
|
3979
|
+
const content = readFileSync2(envPath, "utf-8");
|
|
3980
|
+
for (const line of content.split("\n")) {
|
|
3981
|
+
const trimmed = line.trim();
|
|
3982
|
+
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
3983
|
+
const eqIndex = trimmed.indexOf("=");
|
|
3984
|
+
if (eqIndex === -1) continue;
|
|
3985
|
+
const key = trimmed.slice(0, eqIndex).trim();
|
|
3986
|
+
let value = trimmed.slice(eqIndex + 1).trim();
|
|
3987
|
+
if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) {
|
|
3988
|
+
value = value.slice(1, -1);
|
|
3989
|
+
}
|
|
3990
|
+
if (!process.env[key]) {
|
|
3991
|
+
process.env[key] = value;
|
|
3992
|
+
}
|
|
3993
|
+
}
|
|
3649
3994
|
} catch {
|
|
3650
|
-
return "";
|
|
3651
3995
|
}
|
|
3652
3996
|
}
|
|
3653
|
-
function
|
|
3654
|
-
return
|
|
3655
|
-
|
|
3656
|
-
|
|
3657
|
-
return executeGit("branch --show-current");
|
|
3997
|
+
function resolveEnvVars(value) {
|
|
3998
|
+
return value.replace(ENV_VAR_PATTERN, (_match, varName) => {
|
|
3999
|
+
return process.env[varName] ?? "";
|
|
4000
|
+
});
|
|
3658
4001
|
}
|
|
3659
|
-
|
|
3660
|
-
|
|
3661
|
-
|
|
4002
|
+
|
|
4003
|
+
// src/lib/provider-registry/index.ts
|
|
4004
|
+
init_esm_shims();
|
|
4005
|
+
|
|
4006
|
+
// src/lib/provider-registry/provider-registry.ts
|
|
4007
|
+
init_esm_shims();
|
|
4008
|
+
var AVAILABLE_PROVIDERS = [
|
|
4009
|
+
{
|
|
4010
|
+
id: "fs",
|
|
4011
|
+
name: "\u{1F4C1} File System",
|
|
4012
|
+
description: "Store tasks as Markdown files in a local TASKS/ directory",
|
|
4013
|
+
packageName: "@opentask/taskin-file-system-provider",
|
|
4014
|
+
configSchema: {
|
|
4015
|
+
required: ["tasksDir"],
|
|
4016
|
+
properties: {
|
|
4017
|
+
tasksDir: {
|
|
4018
|
+
type: "string",
|
|
4019
|
+
description: "Directory to store task files"
|
|
4020
|
+
},
|
|
4021
|
+
metadataStyle: {
|
|
4022
|
+
type: "string",
|
|
4023
|
+
description: "Marking of the metadata block for new files (list | hard-break | plain)"
|
|
4024
|
+
}
|
|
4025
|
+
}
|
|
4026
|
+
},
|
|
4027
|
+
status: "stable"
|
|
4028
|
+
},
|
|
4029
|
+
{
|
|
4030
|
+
id: "redmine",
|
|
4031
|
+
name: "\u{1F534} Redmine",
|
|
4032
|
+
description: "Sync tasks with Redmine issues via REST API",
|
|
4033
|
+
packageName: "@opentask/taskin-redmine-provider",
|
|
4034
|
+
configSchema: {
|
|
4035
|
+
required: ["apiUrl", "apiKey", "projectId"],
|
|
4036
|
+
properties: {
|
|
4037
|
+
apiUrl: {
|
|
4038
|
+
type: "string",
|
|
4039
|
+
description: "Redmine server URL (e.g., https://redmine.example.com)"
|
|
4040
|
+
},
|
|
4041
|
+
apiKey: {
|
|
4042
|
+
type: "string",
|
|
4043
|
+
description: "Your Redmine API key",
|
|
4044
|
+
secret: true
|
|
4045
|
+
},
|
|
4046
|
+
projectId: {
|
|
4047
|
+
type: "string",
|
|
4048
|
+
description: "Project identifier or ID"
|
|
4049
|
+
}
|
|
4050
|
+
}
|
|
4051
|
+
},
|
|
4052
|
+
status: "coming-soon"
|
|
4053
|
+
},
|
|
4054
|
+
{
|
|
4055
|
+
id: "jira",
|
|
4056
|
+
name: "\u{1F535} Jira",
|
|
4057
|
+
description: "Sync tasks with Jira issues via REST API",
|
|
4058
|
+
packageName: "@opentask/taskin-jira-provider",
|
|
4059
|
+
configSchema: {
|
|
4060
|
+
required: ["apiUrl", "email", "apiToken", "projectKey"],
|
|
4061
|
+
properties: {
|
|
4062
|
+
apiUrl: {
|
|
4063
|
+
type: "string",
|
|
4064
|
+
description: "Jira server URL (e.g., https://company.atlassian.net)"
|
|
4065
|
+
},
|
|
4066
|
+
email: {
|
|
4067
|
+
type: "string",
|
|
4068
|
+
description: "Your Atlassian account email"
|
|
4069
|
+
},
|
|
4070
|
+
apiToken: {
|
|
4071
|
+
type: "string",
|
|
4072
|
+
description: "Your Jira API token",
|
|
4073
|
+
secret: true
|
|
4074
|
+
},
|
|
4075
|
+
projectKey: {
|
|
4076
|
+
type: "string",
|
|
4077
|
+
description: "Project key (e.g., PROJ)"
|
|
4078
|
+
}
|
|
4079
|
+
}
|
|
4080
|
+
},
|
|
4081
|
+
status: "coming-soon"
|
|
4082
|
+
},
|
|
4083
|
+
{
|
|
4084
|
+
id: "github",
|
|
4085
|
+
name: "\u{1F419} GitHub Issues",
|
|
4086
|
+
description: "Sync tasks with GitHub Issues",
|
|
4087
|
+
packageName: "@opentask/taskin-github-provider",
|
|
4088
|
+
configSchema: {
|
|
4089
|
+
required: ["owner", "repo", "token"],
|
|
4090
|
+
properties: {
|
|
4091
|
+
owner: {
|
|
4092
|
+
type: "string",
|
|
4093
|
+
description: "Repository owner (username or organization)"
|
|
4094
|
+
},
|
|
4095
|
+
repo: {
|
|
4096
|
+
type: "string",
|
|
4097
|
+
description: "Repository name"
|
|
4098
|
+
},
|
|
4099
|
+
token: {
|
|
4100
|
+
type: "string",
|
|
4101
|
+
description: "GitHub Personal Access Token",
|
|
4102
|
+
secret: true
|
|
4103
|
+
}
|
|
4104
|
+
}
|
|
4105
|
+
},
|
|
4106
|
+
status: "coming-soon"
|
|
4107
|
+
}
|
|
4108
|
+
];
|
|
4109
|
+
function getProviderById(id) {
|
|
4110
|
+
return AVAILABLE_PROVIDERS.find((p) => p.id === id);
|
|
4111
|
+
}
|
|
4112
|
+
function getAllProviders() {
|
|
4113
|
+
return AVAILABLE_PROVIDERS;
|
|
3662
4114
|
}
|
|
3663
4115
|
|
|
3664
|
-
//
|
|
4116
|
+
// src/lib/provider-registry/provider-registry.types.ts
|
|
3665
4117
|
init_esm_shims();
|
|
3666
4118
|
|
|
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 "";
|
|
4119
|
+
// src/lib/provider-factory/provider-factory.ts
|
|
4120
|
+
function expandProviderConfig(config) {
|
|
4121
|
+
const expanded = {};
|
|
4122
|
+
for (const [key, value] of Object.entries(config)) {
|
|
4123
|
+
expanded[key] = typeof value === "string" ? resolveEnvVars(value) : value;
|
|
3693
4124
|
}
|
|
4125
|
+
return expanded;
|
|
3694
4126
|
}
|
|
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
|
|
4127
|
+
var buildFileSystemProvider = async ({ projectRoot, providerConfig, tasksDirOverride }) => {
|
|
4128
|
+
const { FileSystemTaskProvider: FileSystemTaskProvider2, isMetadataStyleId: isMetadataStyleId2, UserRegistry: UserRegistry2 } = await Promise.resolve().then(() => (init_src3(), src_exports));
|
|
4129
|
+
const configuredTasksDir = typeof providerConfig.tasksDir === "string" ? providerConfig.tasksDir : "TASKS";
|
|
4130
|
+
const tasksDir = path7.resolve(projectRoot, tasksDirOverride ?? configuredTasksDir);
|
|
4131
|
+
const userRegistry = new UserRegistry2({ taskinDir: path7.join(projectRoot, ".taskin") });
|
|
4132
|
+
const metadataStyle = isMetadataStyleId2(providerConfig.metadataStyle) ? providerConfig.metadataStyle : void 0;
|
|
4133
|
+
const convertMetadataStyleTo = isMetadataStyleId2(providerConfig.convertMetadataStyleTo) ? providerConfig.convertMetadataStyleTo : void 0;
|
|
4134
|
+
const provider = new FileSystemTaskProvider2(tasksDir, userRegistry, void 0, void 0, {
|
|
4135
|
+
...metadataStyle !== void 0 && { metadataStyle },
|
|
4136
|
+
...convertMetadataStyleTo !== void 0 && { convertMetadataStyleTo }
|
|
4137
|
+
});
|
|
4138
|
+
return { provider, userRegistry };
|
|
4139
|
+
};
|
|
4140
|
+
var PROVIDER_BUILDERS = {
|
|
4141
|
+
fs: buildFileSystemProvider
|
|
4142
|
+
};
|
|
4143
|
+
function unknownProviderError(providerType, builders) {
|
|
4144
|
+
const known = getProviderById(providerType);
|
|
4145
|
+
if (known) {
|
|
4146
|
+
return new Error(
|
|
4147
|
+
`Provider "${providerType}" (${known.name}) is configured in .taskin.json but has no implementation yet (status: ${known.status}).
|
|
4148
|
+
Install/await ${known.packageName}, or change provider.type.`
|
|
3720
4149
|
);
|
|
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
4150
|
}
|
|
3977
|
-
|
|
3978
|
-
|
|
3979
|
-
|
|
3980
|
-
|
|
3981
|
-
|
|
4151
|
+
const buildable = Object.keys(builders).join(", ");
|
|
4152
|
+
const listed = getAllProviders().map((provider) => provider.id).join(", ");
|
|
4153
|
+
return new Error(
|
|
4154
|
+
`Unknown provider.type "${providerType}" in .taskin.json.
|
|
4155
|
+
Usable now: ${buildable}. Known ids: ${listed}.`
|
|
4156
|
+
);
|
|
4157
|
+
}
|
|
4158
|
+
async function resolveTaskProvider(options = {}, builders = PROVIDER_BUILDERS) {
|
|
4159
|
+
const projectRoot = path7.resolve(options.cwd ?? process.cwd());
|
|
4160
|
+
loadDotEnv(projectRoot);
|
|
4161
|
+
const config = new ConfigManager(projectRoot).loadConfig();
|
|
4162
|
+
const providerType = config.provider.type;
|
|
4163
|
+
const build = builders[providerType];
|
|
4164
|
+
if (!build) {
|
|
4165
|
+
throw unknownProviderError(providerType, builders);
|
|
3982
4166
|
}
|
|
3983
|
-
|
|
4167
|
+
const context = {
|
|
4168
|
+
projectRoot,
|
|
4169
|
+
providerConfig: { ...expandProviderConfig(config.provider.config), ...options.configOverrides },
|
|
4170
|
+
...options.tasksDir !== void 0 && { tasksDirOverride: options.tasksDir }
|
|
4171
|
+
};
|
|
4172
|
+
const { provider, userRegistry } = await build(context);
|
|
4173
|
+
await userRegistry.load();
|
|
4174
|
+
return { provider, userRegistry, projectRoot, providerType };
|
|
4175
|
+
}
|
|
3984
4176
|
|
|
3985
|
-
//
|
|
4177
|
+
// src/lib/provider-factory/provider-factory.types.ts
|
|
3986
4178
|
init_esm_shims();
|
|
3987
4179
|
|
|
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) {
|
|
4180
|
+
// src/commands/dashboard.ts
|
|
4181
|
+
var __filename2 = fileURLToPath2(import.meta.url);
|
|
4182
|
+
var __dirname2 = path8.dirname(__filename2);
|
|
4183
|
+
async function startHttpServer(app, startPort, host, maxAttempts = 10) {
|
|
4184
|
+
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
4185
|
+
const tryPort = startPort + attempt;
|
|
4007
4186
|
try {
|
|
4008
|
-
|
|
4009
|
-
|
|
4010
|
-
|
|
4187
|
+
const server = createServer(app);
|
|
4188
|
+
await new Promise((resolve, reject) => {
|
|
4189
|
+
server.once("error", reject);
|
|
4190
|
+
server.listen(tryPort, host, () => resolve());
|
|
4011
4191
|
});
|
|
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 {
|
|
4192
|
+
return { server, port: tryPort };
|
|
4193
|
+
} catch (err) {
|
|
4194
|
+
const nodeErr = err;
|
|
4195
|
+
if (nodeErr.code !== "EADDRINUSE") {
|
|
4196
|
+
throw err;
|
|
4055
4197
|
}
|
|
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;
|
|
4198
|
+
if (attempt < maxAttempts - 1) {
|
|
4199
|
+
info(`Port ${tryPort} is in use, trying port ${tryPort + 1}...`);
|
|
4106
4200
|
}
|
|
4107
|
-
} catch {
|
|
4108
|
-
return false;
|
|
4109
4201
|
}
|
|
4110
4202
|
}
|
|
4111
|
-
|
|
4112
|
-
|
|
4113
|
-
|
|
4114
|
-
|
|
4115
|
-
|
|
4116
|
-
|
|
4117
|
-
|
|
4118
|
-
|
|
4119
|
-
|
|
4120
|
-
|
|
4121
|
-
|
|
4203
|
+
throw new Error(
|
|
4204
|
+
`Could not find an available port after ${maxAttempts} attempts (tried ${startPort}-${startPort + maxAttempts - 1})`
|
|
4205
|
+
);
|
|
4206
|
+
}
|
|
4207
|
+
var dashboardCommand = defineCommand({
|
|
4208
|
+
name: "dashboard",
|
|
4209
|
+
description: "\u{1F4CA} Start the Taskin dashboard with WebSocket server",
|
|
4210
|
+
alias: "dash",
|
|
4211
|
+
options: [
|
|
4212
|
+
{
|
|
4213
|
+
flags: "-p, --port <port>",
|
|
4214
|
+
description: "Vite dev server port",
|
|
4215
|
+
defaultValue: "5173"
|
|
4216
|
+
},
|
|
4217
|
+
{
|
|
4218
|
+
flags: "-w, --ws-port <port>",
|
|
4219
|
+
description: "WebSocket server port",
|
|
4220
|
+
defaultValue: "3001"
|
|
4221
|
+
},
|
|
4222
|
+
{
|
|
4223
|
+
flags: "-h, --host <host>",
|
|
4224
|
+
description: "Host to bind servers",
|
|
4225
|
+
defaultValue: "localhost"
|
|
4226
|
+
},
|
|
4227
|
+
{
|
|
4228
|
+
flags: "-b, --browser",
|
|
4229
|
+
description: "Open browser automatically"
|
|
4230
|
+
},
|
|
4231
|
+
{
|
|
4232
|
+
flags: "--open",
|
|
4233
|
+
description: "Show only open tasks (pending, in-progress, blocked)"
|
|
4234
|
+
},
|
|
4235
|
+
{
|
|
4236
|
+
flags: "--closed",
|
|
4237
|
+
description: "Show only closed tasks (done, canceled)"
|
|
4238
|
+
}
|
|
4239
|
+
],
|
|
4240
|
+
handler: async (options) => {
|
|
4241
|
+
await startDashboard(options);
|
|
4242
|
+
}
|
|
4243
|
+
});
|
|
4244
|
+
async function startDashboard(options) {
|
|
4245
|
+
requireTaskinProject();
|
|
4246
|
+
const host = options.host || "localhost";
|
|
4247
|
+
if (!isValidHost(host)) {
|
|
4248
|
+
error("Security validation failed");
|
|
4249
|
+
error(`Invalid host: ${host}. Must be localhost, a valid IPv4 address, or hostname.`);
|
|
4250
|
+
process.exit(1);
|
|
4122
4251
|
}
|
|
4123
|
-
|
|
4124
|
-
|
|
4125
|
-
|
|
4126
|
-
|
|
4127
|
-
encoding: "utf8",
|
|
4128
|
-
stdio: "pipe"
|
|
4129
|
-
}).trim();
|
|
4130
|
-
} catch {
|
|
4131
|
-
return "";
|
|
4132
|
-
}
|
|
4252
|
+
if (typeof options.port === "string" && !isValidPort(options.port)) {
|
|
4253
|
+
error("Security validation failed");
|
|
4254
|
+
error(`Invalid port: ${options.port}. Must be between 1 and 65535.`);
|
|
4255
|
+
process.exit(1);
|
|
4133
4256
|
}
|
|
4134
|
-
|
|
4135
|
-
|
|
4257
|
+
if (typeof options.wsPort === "string" && !isValidPort(options.wsPort)) {
|
|
4258
|
+
error("Security validation failed");
|
|
4259
|
+
error(`Invalid WebSocket port: ${options.wsPort}. Must be between 1 and 65535.`);
|
|
4260
|
+
process.exit(1);
|
|
4136
4261
|
}
|
|
4137
|
-
|
|
4138
|
-
|
|
4139
|
-
|
|
4140
|
-
|
|
4141
|
-
}
|
|
4142
|
-
|
|
4143
|
-
}
|
|
4262
|
+
const port = typeof options.port === "string" ? parseInt(options.port, 10) : options.port || 5173;
|
|
4263
|
+
const wsPort = typeof options.wsPort === "string" ? parseInt(options.wsPort, 10) : options.wsPort || 3001;
|
|
4264
|
+
if (!isValidPort(port)) {
|
|
4265
|
+
error("Security validation failed");
|
|
4266
|
+
error(`Invalid port: ${port}. Must be between 1 and 65535.`);
|
|
4267
|
+
process.exit(1);
|
|
4144
4268
|
}
|
|
4145
|
-
|
|
4146
|
-
|
|
4147
|
-
|
|
4148
|
-
|
|
4149
|
-
stdio: "ignore"
|
|
4150
|
-
});
|
|
4151
|
-
return true;
|
|
4152
|
-
} catch {
|
|
4153
|
-
return false;
|
|
4154
|
-
}
|
|
4269
|
+
if (!isValidPort(wsPort)) {
|
|
4270
|
+
error("Security validation failed");
|
|
4271
|
+
error(`Invalid WebSocket port: ${wsPort}. Must be between 1 and 65535.`);
|
|
4272
|
+
process.exit(1);
|
|
4155
4273
|
}
|
|
4156
|
-
|
|
4274
|
+
printHeader("Starting Taskin Dashboard", "\u{1F4CA}");
|
|
4275
|
+
try {
|
|
4276
|
+
info("Initializing task provider...");
|
|
4277
|
+
let currentDir = process.cwd();
|
|
4278
|
+
let tasksDir = path8.join(currentDir, "TASKS");
|
|
4157
4279
|
try {
|
|
4158
|
-
|
|
4159
|
-
cwd: this.cwd,
|
|
4160
|
-
stdio: "ignore"
|
|
4161
|
-
});
|
|
4162
|
-
return true;
|
|
4280
|
+
await import("fs").then((fs6) => fs6.promises.access(tasksDir));
|
|
4163
4281
|
} catch {
|
|
4164
|
-
|
|
4282
|
+
while (currentDir !== path8.dirname(currentDir)) {
|
|
4283
|
+
const workspaceFile = path8.join(currentDir, "pnpm-workspace.yaml");
|
|
4284
|
+
try {
|
|
4285
|
+
await import("fs").then((fs6) => fs6.promises.access(workspaceFile));
|
|
4286
|
+
tasksDir = path8.join(currentDir, "TASKS");
|
|
4287
|
+
break;
|
|
4288
|
+
} catch {
|
|
4289
|
+
currentDir = path8.dirname(currentDir);
|
|
4290
|
+
}
|
|
4291
|
+
}
|
|
4165
4292
|
}
|
|
4166
|
-
|
|
4167
|
-
|
|
4168
|
-
|
|
4169
|
-
|
|
4170
|
-
|
|
4171
|
-
|
|
4172
|
-
|
|
4173
|
-
|
|
4174
|
-
|
|
4175
|
-
|
|
4293
|
+
info(`Using tasks directory: ${tasksDir}`);
|
|
4294
|
+
const monorepoRoot = path8.dirname(tasksDir);
|
|
4295
|
+
const { provider } = await resolveTaskProvider({ cwd: monorepoRoot });
|
|
4296
|
+
const manager = new TaskManager(provider);
|
|
4297
|
+
info(`Starting WebSocket server on ${host}:${wsPort}...`);
|
|
4298
|
+
const wsServer = new TaskWebSocketServer({
|
|
4299
|
+
taskManager: manager,
|
|
4300
|
+
taskProvider: provider,
|
|
4301
|
+
options: {
|
|
4302
|
+
port: wsPort,
|
|
4303
|
+
host
|
|
4304
|
+
}
|
|
4305
|
+
});
|
|
4306
|
+
await wsServer.start();
|
|
4307
|
+
success(`\u2713 WebSocket server running on ws://${host}:${wsPort}`);
|
|
4308
|
+
info(`Starting dashboard server on http://${host}:${port}...`);
|
|
4309
|
+
const isDev = __dirname2.includes("/src/");
|
|
4310
|
+
const dashboardDist = isDev ? path8.join(__dirname2, "..", "..", "dashboard-dist") : path8.join(__dirname2, "..", "dashboard-dist");
|
|
4311
|
+
const app = express();
|
|
4312
|
+
app.disable("x-powered-by");
|
|
4313
|
+
app.use((_req, res, next) => {
|
|
4314
|
+
res.setHeader("X-Frame-Options", "DENY");
|
|
4315
|
+
res.setHeader("X-Content-Type-Options", "nosniff");
|
|
4316
|
+
res.setHeader("X-XSS-Protection", "1; mode=block");
|
|
4317
|
+
res.setHeader(
|
|
4318
|
+
"Content-Security-Policy",
|
|
4319
|
+
"default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; connect-src 'self' ws: wss:;"
|
|
4320
|
+
);
|
|
4321
|
+
next();
|
|
4322
|
+
});
|
|
4323
|
+
app.use((req, res, next) => {
|
|
4324
|
+
if (req.path === "/" || req.path === "/index.html") {
|
|
4325
|
+
import("fs").then((fs6) => fs6.promises.readFile(path8.join(dashboardDist, "index.html"), "utf-8")).then((html) => {
|
|
4326
|
+
const safeHost = escapeHtml(host);
|
|
4327
|
+
const safeWsPort = escapeHtml(String(wsPort));
|
|
4328
|
+
const injectedHtml = html.replace(
|
|
4329
|
+
"</head>",
|
|
4330
|
+
`<script>window.VITE_WS_URL = 'ws://${safeHost}:${safeWsPort}';</script></head>`
|
|
4331
|
+
);
|
|
4332
|
+
res.send(injectedHtml);
|
|
4333
|
+
}).catch((err) => {
|
|
4334
|
+
console.error("Failed to read index.html:", err);
|
|
4335
|
+
res.status(500).send("Internal Server Error");
|
|
4336
|
+
});
|
|
4337
|
+
} else {
|
|
4338
|
+
next();
|
|
4339
|
+
}
|
|
4340
|
+
});
|
|
4341
|
+
app.use(
|
|
4342
|
+
express.static(dashboardDist, {
|
|
4343
|
+
dotfiles: "deny",
|
|
4344
|
+
// Deny access to dotfiles
|
|
4345
|
+
index: false,
|
|
4346
|
+
// Don't serve index.html here (handled above)
|
|
4347
|
+
redirect: false
|
|
4348
|
+
// Don't redirect to trailing slash
|
|
4349
|
+
})
|
|
4350
|
+
);
|
|
4351
|
+
app.use((_req, res) => {
|
|
4352
|
+
res.status(404).send("Not Found");
|
|
4353
|
+
});
|
|
4354
|
+
const { server: httpServer, port: actualPort } = await startHttpServer(app, port, host);
|
|
4355
|
+
if (actualPort !== port) {
|
|
4356
|
+
warning(`Port ${port} was in use. Dashboard started on port ${actualPort}.`);
|
|
4176
4357
|
}
|
|
4177
|
-
|
|
4178
|
-
|
|
4179
|
-
|
|
4180
|
-
|
|
4181
|
-
|
|
4182
|
-
|
|
4183
|
-
});
|
|
4184
|
-
return true;
|
|
4185
|
-
} catch {
|
|
4186
|
-
return false;
|
|
4358
|
+
success(`\u2713 Dashboard available at http://${host}:${actualPort}`);
|
|
4359
|
+
const filterParams = new URLSearchParams();
|
|
4360
|
+
if (options.open) {
|
|
4361
|
+
filterParams.set("filter", "open");
|
|
4362
|
+
} else if (options.closed) {
|
|
4363
|
+
filterParams.set("filter", "closed");
|
|
4187
4364
|
}
|
|
4188
|
-
|
|
4189
|
-
|
|
4190
|
-
|
|
4191
|
-
|
|
4192
|
-
|
|
4193
|
-
|
|
4365
|
+
const filterQuery = filterParams.toString() ? `?${filterParams.toString()}` : "";
|
|
4366
|
+
if (options.browser) {
|
|
4367
|
+
const url = `http://${host}:${actualPort}${filterQuery}`;
|
|
4368
|
+
await import("child_process").then((cp) => {
|
|
4369
|
+
const cmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
|
|
4370
|
+
cp.exec(`${cmd} ${url}`);
|
|
4194
4371
|
});
|
|
4195
|
-
return true;
|
|
4196
|
-
} catch {
|
|
4197
|
-
return false;
|
|
4198
4372
|
}
|
|
4199
|
-
|
|
4200
|
-
|
|
4201
|
-
|
|
4202
|
-
|
|
4203
|
-
|
|
4204
|
-
|
|
4373
|
+
info("");
|
|
4374
|
+
info(chalk4.bold("Dashboard Controls:"));
|
|
4375
|
+
info(` \u2022 Dashboard: ${chalk4.cyan(`http://${host}:${actualPort}${filterQuery}`)}`);
|
|
4376
|
+
info(` \u2022 WebSocket: ${chalk4.cyan(`ws://${host}:${wsPort}`)}`);
|
|
4377
|
+
if (options.open) {
|
|
4378
|
+
info(` \u2022 Filter: ${chalk4.yellow("Open tasks only")}`);
|
|
4379
|
+
} else if (options.closed) {
|
|
4380
|
+
info(` \u2022 Filter: ${chalk4.yellow("Closed tasks only")}`);
|
|
4381
|
+
}
|
|
4382
|
+
info(` \u2022 Press ${chalk4.bold("Ctrl+C")} to stop both servers`);
|
|
4383
|
+
info("");
|
|
4384
|
+
const cleanup = async () => {
|
|
4385
|
+
info("\nShutting down servers...");
|
|
4386
|
+
await new Promise((resolve) => {
|
|
4387
|
+
httpServer.close(() => resolve());
|
|
4205
4388
|
});
|
|
4206
|
-
|
|
4207
|
-
|
|
4208
|
-
|
|
4389
|
+
await wsServer.stop();
|
|
4390
|
+
success("\u2713 Servers stopped");
|
|
4391
|
+
process.exit(0);
|
|
4392
|
+
};
|
|
4393
|
+
process.on("SIGINT", cleanup);
|
|
4394
|
+
process.on("SIGTERM", cleanup);
|
|
4395
|
+
} catch (err) {
|
|
4396
|
+
error("Failed to start dashboard");
|
|
4397
|
+
if (err instanceof Error) {
|
|
4398
|
+
error(err.message);
|
|
4209
4399
|
}
|
|
4400
|
+
process.exit(1);
|
|
4210
4401
|
}
|
|
4211
|
-
}
|
|
4212
|
-
|
|
4213
|
-
// ../git-utils/src/git-service.types.ts
|
|
4214
|
-
init_esm_shims();
|
|
4402
|
+
}
|
|
4215
4403
|
|
|
4216
4404
|
// src/commands/export.ts
|
|
4405
|
+
init_esm_shims();
|
|
4406
|
+
init_src3();
|
|
4407
|
+
init_src();
|
|
4217
4408
|
import fs5 from "fs/promises";
|
|
4218
4409
|
import path9 from "path";
|
|
4219
4410
|
function userStatsToCsv(stats) {
|
|
@@ -4291,7 +4482,8 @@ function registerExportCommand(program2) {
|
|
|
4291
4482
|
|
|
4292
4483
|
// src/commands/finish.ts
|
|
4293
4484
|
init_esm_shims();
|
|
4294
|
-
|
|
4485
|
+
init_src3();
|
|
4486
|
+
init_src();
|
|
4295
4487
|
import { execSync as execSync5 } from "child_process";
|
|
4296
4488
|
|
|
4297
4489
|
// src/lib/notification/notify-helper.ts
|
|
@@ -4713,6 +4905,14 @@ async function finishTask(taskId, options, gitService) {
|
|
|
4713
4905
|
}
|
|
4714
4906
|
info(`Found task: ${task.title}`);
|
|
4715
4907
|
info(`Current status: ${task.status}`);
|
|
4908
|
+
const configManager = new ConfigManager(monorepoRoot);
|
|
4909
|
+
const behavior = configManager.getAutomationBehavior();
|
|
4910
|
+
const autoSyncActive = behavior.autoSync && !!behavior.defaultBranch;
|
|
4911
|
+
const statusCommitMessage = buildTaskStatusCommitMessage({
|
|
4912
|
+
taskId: normalizedId,
|
|
4913
|
+
status: "done",
|
|
4914
|
+
ciSkipTag: behavior.ciSkipTag
|
|
4915
|
+
});
|
|
4716
4916
|
if (options.dryRun) {
|
|
4717
4917
|
console.log();
|
|
4718
4918
|
info("\u{1F50D} Dry run mode - showing what would be executed:");
|
|
@@ -4724,7 +4924,7 @@ async function finishTask(taskId, options, gitService) {
|
|
|
4724
4924
|
info("Git operations:");
|
|
4725
4925
|
console.log(
|
|
4726
4926
|
colors.secondary(
|
|
4727
|
-
` - Commit status: git add TASKS/task-${normalizedId}-*.md && git commit -m "
|
|
4927
|
+
` - Commit status: git add TASKS/task-${normalizedId}-*.md && git commit -m "${statusCommitMessage}"`
|
|
4728
4928
|
)
|
|
4729
4929
|
);
|
|
4730
4930
|
console.log(
|
|
@@ -4741,13 +4941,10 @@ async function finishTask(taskId, options, gitService) {
|
|
|
4741
4941
|
error("Task is already done");
|
|
4742
4942
|
process.exit(1);
|
|
4743
4943
|
}
|
|
4744
|
-
const configManager = new ConfigManager(monorepoRoot);
|
|
4745
|
-
const behavior = configManager.getAutomationBehavior();
|
|
4746
|
-
const autoSyncActive = behavior.autoSync && !!behavior.defaultBranch;
|
|
4747
4944
|
if (behavior.autoSync && !behavior.defaultBranch) {
|
|
4748
4945
|
warning("autoSync is enabled but no defaultBranch is configured. Nothing will be synced.");
|
|
4749
4946
|
}
|
|
4750
|
-
const git = gitService ?? new GitService(process.cwd());
|
|
4947
|
+
const git = gitService ?? new GitService(process.cwd(), { ciSkipTag: behavior.ciSkipTag });
|
|
4751
4948
|
if (!options.skipUpdate) {
|
|
4752
4949
|
info("Marking task as done...");
|
|
4753
4950
|
const updatedTask = await taskManager.finishTask(task.id);
|
|
@@ -4764,7 +4961,8 @@ async function finishTask(taskId, options, gitService) {
|
|
|
4764
4961
|
const squashed = await squashTaskFileOnDone(git, {
|
|
4765
4962
|
taskId: normalizedId,
|
|
4766
4963
|
defaultBranch: behavior.defaultBranch,
|
|
4767
|
-
originBranch: behavior.originBranch
|
|
4964
|
+
originBranch: behavior.originBranch,
|
|
4965
|
+
ciSkipTag: behavior.ciSkipTag
|
|
4768
4966
|
});
|
|
4769
4967
|
if (squashed) {
|
|
4770
4968
|
success(`\u2713 Squash commit pushed to ${behavior.originBranch}`);
|
|
@@ -4798,7 +4996,7 @@ async function finishTask(taskId, options, gitService) {
|
|
|
4798
4996
|
if (!options.skipUpdate) {
|
|
4799
4997
|
if (!behavior.autoCommitStatusChange) {
|
|
4800
4998
|
steps.push(
|
|
4801
|
-
`Commit the status change: git add TASKS/task-${normalizedId}-*.md && git commit -m "
|
|
4999
|
+
`Commit the status change: git add TASKS/task-${normalizedId}-*.md && git commit -m "${statusCommitMessage}"`
|
|
4802
5000
|
);
|
|
4803
5001
|
}
|
|
4804
5002
|
steps.push("Review your changes");
|
|
@@ -4839,6 +5037,7 @@ async function finishTask(taskId, options, gitService) {
|
|
|
4839
5037
|
|
|
4840
5038
|
// src/commands/init.ts
|
|
4841
5039
|
init_esm_shims();
|
|
5040
|
+
init_src();
|
|
4842
5041
|
import { existsSync as existsSync5, readdirSync, writeFileSync as writeFileSync2 } from "fs";
|
|
4843
5042
|
import inquirer2 from "inquirer";
|
|
4844
5043
|
import { join as join3 } from "path";
|
|
@@ -4929,6 +5128,7 @@ async function ensureProviderInstalled(provider) {
|
|
|
4929
5128
|
init_esm_shims();
|
|
4930
5129
|
|
|
4931
5130
|
// src/commands/init.ts
|
|
5131
|
+
var NO_CI_SKIP_TAG_KEYWORD2 = "none";
|
|
4932
5132
|
var initCommand = defineCommand({
|
|
4933
5133
|
name: "init",
|
|
4934
5134
|
alias: "setup",
|
|
@@ -4941,6 +5141,10 @@ var initCommand = defineCommand({
|
|
|
4941
5141
|
{
|
|
4942
5142
|
flags: "-p, --provider <provider>",
|
|
4943
5143
|
description: "Provider to use (fs, redmine, jira, github) - skips interactive prompt"
|
|
5144
|
+
},
|
|
5145
|
+
{
|
|
5146
|
+
flags: "--ci-skip-tag <tag>",
|
|
5147
|
+
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
5148
|
}
|
|
4945
5149
|
],
|
|
4946
5150
|
handler: async (options) => {
|
|
@@ -4999,8 +5203,14 @@ async function initializeTaskin(options) {
|
|
|
4999
5203
|
await ensureProviderInstalled(selectedProvider);
|
|
5000
5204
|
}
|
|
5001
5205
|
const providerConfig = await setupProviderConfig(selectedProvider, cwd);
|
|
5206
|
+
const ciSkipTag = await resolveCiSkipTag(options.ciSkipTag);
|
|
5002
5207
|
const config = {
|
|
5003
5208
|
version: "1.0.3",
|
|
5209
|
+
automation: {
|
|
5210
|
+
level: "assisted",
|
|
5211
|
+
autoSync: true,
|
|
5212
|
+
ciSkipTag
|
|
5213
|
+
},
|
|
5004
5214
|
provider: {
|
|
5005
5215
|
type: selectedProvider.id,
|
|
5006
5216
|
config: providerConfig
|
|
@@ -5027,6 +5237,51 @@ async function initializeTaskin(options) {
|
|
|
5027
5237
|
info("For more information, run: taskin --help");
|
|
5028
5238
|
console.log();
|
|
5029
5239
|
}
|
|
5240
|
+
async function resolveCiSkipTag(fromFlag) {
|
|
5241
|
+
if (fromFlag !== void 0) {
|
|
5242
|
+
return normalizeCiSkipTag(fromFlag);
|
|
5243
|
+
}
|
|
5244
|
+
if (process.env.CI === "true") {
|
|
5245
|
+
return DEFAULT_CI_SKIP_TAG;
|
|
5246
|
+
}
|
|
5247
|
+
console.log();
|
|
5248
|
+
info("Taskin appends a tag to the commits it writes itself \u2014 status changes and");
|
|
5249
|
+
info("task files \u2014 so they do not trigger your pipeline.");
|
|
5250
|
+
console.log();
|
|
5251
|
+
const { choice } = await inquirer2.prompt([
|
|
5252
|
+
{
|
|
5253
|
+
type: "list",
|
|
5254
|
+
name: "choice",
|
|
5255
|
+
message: "Tag for Taskin commits:",
|
|
5256
|
+
default: DEFAULT_CI_SKIP_TAG,
|
|
5257
|
+
choices: [
|
|
5258
|
+
{ name: `${CI_SKIP_TAGS[0]} \u2014 GitHub, GitLab and Bitbucket (recommended)`, value: CI_SKIP_TAGS[0] },
|
|
5259
|
+
{ name: `${CI_SKIP_TAGS[1]} \u2014 GitHub, GitLab and Bitbucket`, value: CI_SKIP_TAGS[1] },
|
|
5260
|
+
{ name: `${CI_SKIP_TAGS[2]} \u2014 GitHub Actions only`, value: CI_SKIP_TAGS[2] },
|
|
5261
|
+
{ name: `${CI_SKIP_TAGS[3]} \u2014 GitHub Actions only`, value: CI_SKIP_TAGS[3] },
|
|
5262
|
+
{ name: `${CI_SKIP_TAGS[4]} \u2014 GitHub Actions only`, value: CI_SKIP_TAGS[4] },
|
|
5263
|
+
{ name: "none \u2014 do not mark the commits, let CI run", value: NO_CI_SKIP_TAG_KEYWORD2 },
|
|
5264
|
+
{ name: "custom\u2026 \u2014 another CI (Azure DevOps uses ***NO_CI***)", value: "custom" }
|
|
5265
|
+
]
|
|
5266
|
+
}
|
|
5267
|
+
]);
|
|
5268
|
+
if (choice !== "custom") {
|
|
5269
|
+
return normalizeCiSkipTag(choice);
|
|
5270
|
+
}
|
|
5271
|
+
const { customTag } = await inquirer2.prompt([
|
|
5272
|
+
{
|
|
5273
|
+
type: "input",
|
|
5274
|
+
name: "customTag",
|
|
5275
|
+
message: "Tag to append:",
|
|
5276
|
+
default: DEFAULT_CI_SKIP_TAG
|
|
5277
|
+
}
|
|
5278
|
+
]);
|
|
5279
|
+
return normalizeCiSkipTag(customTag);
|
|
5280
|
+
}
|
|
5281
|
+
function normalizeCiSkipTag(input) {
|
|
5282
|
+
const trimmed = input.trim();
|
|
5283
|
+
return trimmed.toLowerCase() === NO_CI_SKIP_TAG_KEYWORD2 ? "" : trimmed;
|
|
5284
|
+
}
|
|
5030
5285
|
async function setupProviderConfig(provider, cwd) {
|
|
5031
5286
|
if (provider.id === "fs") {
|
|
5032
5287
|
return setupFileSystemProvider(cwd);
|
|
@@ -5051,7 +5306,7 @@ async function setupProviderConfig(provider, cwd) {
|
|
|
5051
5306
|
}
|
|
5052
5307
|
async function setupFileSystemProvider(cwd) {
|
|
5053
5308
|
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(() => (
|
|
5309
|
+
const { DEFAULT_METADATA_STYLE_ID: DEFAULT_METADATA_STYLE_ID2, FileSystemTaskProvider: FileSystemTaskProvider2, getMetadataStyle: getMetadataStyle2, UserRegistry: UserRegistry2 } = await Promise.resolve().then(() => (init_src3(), src_exports));
|
|
5055
5310
|
const userRegistry = new UserRegistry2({ taskinDir: join3(cwd, ".taskin") });
|
|
5056
5311
|
const fileSystemProvider = new FileSystemTaskProvider2(tasksDir, userRegistry);
|
|
5057
5312
|
await fileSystemProvider.initialize();
|
|
@@ -5096,7 +5351,7 @@ You can edit or delete this file. Use \`taskin list\` to see all tasks.
|
|
|
5096
5351
|
};
|
|
5097
5352
|
}
|
|
5098
5353
|
async function promptCreateFirstUser(cwd) {
|
|
5099
|
-
const { UserRegistry: UserRegistry2 } = await Promise.resolve().then(() => (
|
|
5354
|
+
const { UserRegistry: UserRegistry2 } = await Promise.resolve().then(() => (init_src3(), src_exports));
|
|
5100
5355
|
const taskinDir = join3(cwd, ".taskin");
|
|
5101
5356
|
const userRegistry = new UserRegistry2({ taskinDir });
|
|
5102
5357
|
const { createFirstUser } = await inquirer2.prompt([
|
|
@@ -5862,7 +6117,8 @@ async function startMCPServer(options) {
|
|
|
5862
6117
|
|
|
5863
6118
|
// src/commands/new.ts
|
|
5864
6119
|
init_esm_shims();
|
|
5865
|
-
|
|
6120
|
+
init_src3();
|
|
6121
|
+
init_src();
|
|
5866
6122
|
import { TASK_TYPES as TASK_TYPES2 } from "@opentask/taskin-types";
|
|
5867
6123
|
import inquirer3 from "inquirer";
|
|
5868
6124
|
var createCommand = defineCommand({
|
|
@@ -5964,7 +6220,7 @@ async function createTask(options, gitService) {
|
|
|
5964
6220
|
if (behavior.autoSync && !behavior.defaultBranch) {
|
|
5965
6221
|
warning("autoSync is enabled but no defaultBranch is configured. Nothing will be synced.");
|
|
5966
6222
|
}
|
|
5967
|
-
const git = gitService ?? new GitService(process.cwd());
|
|
6223
|
+
const git = gitService ?? new GitService(process.cwd(), { ciSkipTag: behavior.ciSkipTag });
|
|
5968
6224
|
if (autoSyncActive) {
|
|
5969
6225
|
try {
|
|
5970
6226
|
await syncBeforeCreate(git, {
|
|
@@ -5996,7 +6252,8 @@ async function createTask(options, gitService) {
|
|
|
5996
6252
|
await pushAfterCreate(git, {
|
|
5997
6253
|
taskId,
|
|
5998
6254
|
title: options.title,
|
|
5999
|
-
defaultBranch: behavior.defaultBranch
|
|
6255
|
+
defaultBranch: behavior.defaultBranch,
|
|
6256
|
+
ciSkipTag: behavior.ciSkipTag
|
|
6000
6257
|
});
|
|
6001
6258
|
} catch (pushError) {
|
|
6002
6259
|
error(
|
|
@@ -6216,6 +6473,7 @@ async function pauseTask(taskId, options) {
|
|
|
6216
6473
|
|
|
6217
6474
|
// src/commands/review.ts
|
|
6218
6475
|
init_esm_shims();
|
|
6476
|
+
init_src();
|
|
6219
6477
|
import { execSync as execSync9 } from "child_process";
|
|
6220
6478
|
|
|
6221
6479
|
// src/lib/hook-runner.ts
|
|
@@ -6441,10 +6699,14 @@ async function reviewTask(taskId, options) {
|
|
|
6441
6699
|
success(`\u2713 Task ${updatedTask.id} status changed to: ${updatedTask.status}`);
|
|
6442
6700
|
if (behavior.autoCommitStatusChange) {
|
|
6443
6701
|
try {
|
|
6444
|
-
|
|
6445
|
-
`
|
|
6446
|
-
|
|
6702
|
+
const message = appendCiSkipTag(
|
|
6703
|
+
`docs(TASKS): task-${normalizedId} - mark as ready for review`,
|
|
6704
|
+
behavior.ciSkipTag
|
|
6447
6705
|
);
|
|
6706
|
+
execSync9(`git add TASKS/task-${normalizedId}-*.md && git commit -m "${message}"`, {
|
|
6707
|
+
cwd: monorepoRoot,
|
|
6708
|
+
stdio: "ignore"
|
|
6709
|
+
});
|
|
6448
6710
|
success("\u2713 Auto-committed status change");
|
|
6449
6711
|
} catch {
|
|
6450
6712
|
}
|
|
@@ -6492,6 +6754,7 @@ async function reviewTask(taskId, options) {
|
|
|
6492
6754
|
|
|
6493
6755
|
// src/commands/start.ts
|
|
6494
6756
|
init_esm_shims();
|
|
6757
|
+
init_src();
|
|
6495
6758
|
var startCommand = defineCommand({
|
|
6496
6759
|
name: "start <task-id>",
|
|
6497
6760
|
description: "\u{1F680} Start working on a task",
|
|
@@ -6536,6 +6799,13 @@ async function startTask(taskId, _options, gitService) {
|
|
|
6536
6799
|
}
|
|
6537
6800
|
info(`Found task: ${task.title}`);
|
|
6538
6801
|
info(`Current status: ${task.status}`);
|
|
6802
|
+
const configManager = new ConfigManager(monorepoRoot);
|
|
6803
|
+
const behavior = configManager.getAutomationBehavior();
|
|
6804
|
+
const statusCommitMessage = buildTaskStatusCommitMessage({
|
|
6805
|
+
taskId: normalizedId,
|
|
6806
|
+
status: "in-progress",
|
|
6807
|
+
ciSkipTag: behavior.ciSkipTag
|
|
6808
|
+
});
|
|
6539
6809
|
if (_options.dryRun) {
|
|
6540
6810
|
console.log();
|
|
6541
6811
|
info("\u{1F50D} Dry run mode - showing what would be executed:");
|
|
@@ -6547,7 +6817,7 @@ async function startTask(taskId, _options, gitService) {
|
|
|
6547
6817
|
console.log(colors.secondary(` - Create branch: git checkout -b feat/task-${normalizedId}`));
|
|
6548
6818
|
console.log(
|
|
6549
6819
|
colors.secondary(
|
|
6550
|
-
` - Commit status: git add TASKS/task-${normalizedId}-*.md && git commit -m "
|
|
6820
|
+
` - Commit status: git add TASKS/task-${normalizedId}-*.md && git commit -m "${statusCommitMessage}"`
|
|
6551
6821
|
)
|
|
6552
6822
|
);
|
|
6553
6823
|
console.log();
|
|
@@ -6566,9 +6836,7 @@ async function startTask(taskId, _options, gitService) {
|
|
|
6566
6836
|
const updatedTask = await taskManager.startTask(task.id);
|
|
6567
6837
|
success(`Task ${updatedTask.id} started successfully!`);
|
|
6568
6838
|
success(`Status changed to: ${updatedTask.status}`);
|
|
6569
|
-
const
|
|
6570
|
-
const behavior = configManager.getAutomationBehavior();
|
|
6571
|
-
const git = gitService ?? new GitService(process.cwd());
|
|
6839
|
+
const git = gitService ?? new GitService(process.cwd(), { ciSkipTag: behavior.ciSkipTag });
|
|
6572
6840
|
if (behavior.autoCommitStatusChange) {
|
|
6573
6841
|
const committed = await git.commitTaskStatusChangeOnBranch(normalizedId, "in-progress", behavior.defaultBranch);
|
|
6574
6842
|
if (committed) {
|
|
@@ -6580,7 +6848,7 @@ async function startTask(taskId, _options, gitService) {
|
|
|
6580
6848
|
info("Next steps (suggestions):");
|
|
6581
6849
|
console.log(
|
|
6582
6850
|
colors.secondary(
|
|
6583
|
-
` 1. Commit the status change: git add TASKS/task-${normalizedId}-*.md && git commit -m "
|
|
6851
|
+
` 1. Commit the status change: git add TASKS/task-${normalizedId}-*.md && git commit -m "${statusCommitMessage}"`
|
|
6584
6852
|
)
|
|
6585
6853
|
);
|
|
6586
6854
|
console.log(colors.secondary(` 2. Create a branch: git checkout -b feat/task-${normalizedId}`));
|
|
@@ -6601,7 +6869,8 @@ async function startTask(taskId, _options, gitService) {
|
|
|
6601
6869
|
|
|
6602
6870
|
// src/commands/stats.ts
|
|
6603
6871
|
init_esm_shims();
|
|
6604
|
-
|
|
6872
|
+
init_src3();
|
|
6873
|
+
init_src();
|
|
6605
6874
|
import chalk7 from "chalk";
|
|
6606
6875
|
import path11 from "path";
|
|
6607
6876
|
var statsCommand = defineCommand({
|
|
@@ -6943,7 +7212,7 @@ init_esm_shims();
|
|
|
6943
7212
|
|
|
6944
7213
|
// src/lib/file-system-task-linter/file-system-task-linter.ts
|
|
6945
7214
|
init_esm_shims();
|
|
6946
|
-
|
|
7215
|
+
init_src3();
|
|
6947
7216
|
import { TASK_STATUSES as TASK_STATUSES3, TASK_TYPES as TASK_TYPES3 } from "@opentask/taskin-types";
|
|
6948
7217
|
import chalk8 from "chalk";
|
|
6949
7218
|
import { readdir, readFile as readFile2 } from "fs/promises";
|