taskin 4.1.0 → 4.1.2

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