taskin 1.0.11 → 1.0.12
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 +560 -495
- package/package.json +4 -4
package/dist/index.js
CHANGED
|
@@ -51,6 +51,48 @@ var init_esm_shims = __esm({
|
|
|
51
51
|
}
|
|
52
52
|
});
|
|
53
53
|
|
|
54
|
+
// ../fs-task-provider/dist/i18n.js
|
|
55
|
+
function getI18n(locale = "en-US") {
|
|
56
|
+
return i18nConfig[locale];
|
|
57
|
+
}
|
|
58
|
+
function detectLocale(content) {
|
|
59
|
+
if (content.includes("## Descri\xE7\xE3o") || content.includes("## Tipo") || content.includes("## Respons\xE1vel") || content.includes("## Tarefas")) {
|
|
60
|
+
return "pt-BR";
|
|
61
|
+
}
|
|
62
|
+
return "en-US";
|
|
63
|
+
}
|
|
64
|
+
var i18nConfig;
|
|
65
|
+
var init_i18n = __esm({
|
|
66
|
+
"../fs-task-provider/dist/i18n.js"() {
|
|
67
|
+
"use strict";
|
|
68
|
+
init_esm_shims();
|
|
69
|
+
i18nConfig = {
|
|
70
|
+
"en-US": {
|
|
71
|
+
status: "Status",
|
|
72
|
+
type: "Type",
|
|
73
|
+
assignee: "Assignee",
|
|
74
|
+
description: "Description",
|
|
75
|
+
tasks: "Tasks",
|
|
76
|
+
notes: "Notes",
|
|
77
|
+
defaultAssignee: "To be defined",
|
|
78
|
+
descriptionPlaceholder: "Add task description here...",
|
|
79
|
+
notesPlaceholder: "Add any relevant notes or links here."
|
|
80
|
+
},
|
|
81
|
+
"pt-BR": {
|
|
82
|
+
status: "Status",
|
|
83
|
+
type: "Tipo",
|
|
84
|
+
assignee: "Respons\xE1vel",
|
|
85
|
+
description: "Descri\xE7\xE3o",
|
|
86
|
+
tasks: "Tarefas",
|
|
87
|
+
notes: "Notas",
|
|
88
|
+
defaultAssignee: "A definir",
|
|
89
|
+
descriptionPlaceholder: "Adicione a descri\xE7\xE3o da tarefa aqui...",
|
|
90
|
+
notesPlaceholder: "Adicione notas ou links relevantes aqui."
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
|
|
54
96
|
// ../fs-task-provider/dist/task-validator.js
|
|
55
97
|
var task_validator_exports = {};
|
|
56
98
|
__export(task_validator_exports, {
|
|
@@ -62,47 +104,79 @@ import { readFile, writeFile } from "fs/promises";
|
|
|
62
104
|
async function fixTaskFile(filePath) {
|
|
63
105
|
try {
|
|
64
106
|
const content = await readFile(filePath, "utf-8");
|
|
65
|
-
const
|
|
66
|
-
const
|
|
67
|
-
const
|
|
68
|
-
|
|
107
|
+
const locale = detectLocale(content);
|
|
108
|
+
const i18n = getI18n(locale);
|
|
109
|
+
const statusPattern = new RegExp(`##\\s*(?:Status|${i18n.status})\\s*\\n\\s*([^\\n\\r]+)`, "i");
|
|
110
|
+
const typePattern = new RegExp(`##\\s*(?:Type|${i18n.type})\\s*\\n\\s*([^\\n\\r]+)`, "i");
|
|
111
|
+
const assigneePattern = new RegExp(`##\\s*(?:Assignee|${i18n.assignee})\\s*\\n\\s*([^\\n\\r]+)`, "i");
|
|
112
|
+
const hasSectionStatus = statusPattern.test(content);
|
|
113
|
+
const hasSectionType = typePattern.test(content);
|
|
114
|
+
const hasSectionAssignee = assigneePattern.test(content);
|
|
115
|
+
const inlineStatusPattern = /^(Status|Tipo):\s*(.+?)(\s*)$/im;
|
|
116
|
+
const inlineTypePattern = /^(Type|Tipo):\s*(.+?)(\s*)$/im;
|
|
117
|
+
const inlineAssigneePattern = /^(Assignee|Responsável):\s*(.+?)(\s*)$/im;
|
|
118
|
+
const hasInlineStatus = inlineStatusPattern.test(content);
|
|
119
|
+
const hasInlineType = inlineTypePattern.test(content);
|
|
120
|
+
const hasInlineAssignee = inlineAssigneePattern.test(content);
|
|
121
|
+
const needsSpaceFix = hasInlineStatus && !/^(?:Status|Tipo):.+ $/im.test(content) || hasInlineType && !/^(?:Type|Tipo):.+ $/im.test(content) || hasInlineAssignee && !/^(?:Assignee|Responsável):.+ $/im.test(content);
|
|
122
|
+
if (!hasSectionStatus && !hasSectionType && !hasSectionAssignee && !needsSpaceFix) {
|
|
69
123
|
return false;
|
|
70
124
|
}
|
|
71
|
-
const statusMatch = content.match(/^Status:\s*(.+)$/im);
|
|
72
|
-
const typeMatch = content.match(/^Type:\s*(.+)$/im);
|
|
73
|
-
const assigneeMatch = content.match(/^Assignee:\s*(.+)$/im);
|
|
74
125
|
let newContent = content;
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
126
|
+
let wasModified = false;
|
|
127
|
+
if (hasSectionStatus || hasSectionType || hasSectionAssignee) {
|
|
128
|
+
const statusMatch = content.match(statusPattern);
|
|
129
|
+
const typeMatch = content.match(typePattern);
|
|
130
|
+
const assigneeMatch = content.match(assigneePattern);
|
|
131
|
+
if (statusMatch) {
|
|
132
|
+
newContent = newContent.replace(statusPattern, "");
|
|
133
|
+
}
|
|
134
|
+
if (typeMatch) {
|
|
135
|
+
newContent = newContent.replace(typePattern, "");
|
|
136
|
+
}
|
|
137
|
+
if (assigneeMatch) {
|
|
138
|
+
newContent = newContent.replace(assigneePattern, "");
|
|
139
|
+
}
|
|
140
|
+
newContent = newContent.replace(/\n{3,}/g, "\n\n");
|
|
141
|
+
const titleLineIdx = newContent.split("\n").findIndex((line) => line.trim().startsWith("# "));
|
|
142
|
+
if (titleLineIdx === -1) {
|
|
143
|
+
return false;
|
|
144
|
+
}
|
|
145
|
+
const contentLines = newContent.split("\n");
|
|
146
|
+
const beforeTitle = contentLines.slice(0, titleLineIdx + 1);
|
|
147
|
+
const afterTitle = contentLines.slice(titleLineIdx + 1);
|
|
148
|
+
const inlineMetadata = [];
|
|
149
|
+
if (statusMatch) {
|
|
150
|
+
inlineMetadata.push(`Status: ${statusMatch[1].trim()} `);
|
|
151
|
+
}
|
|
152
|
+
if (typeMatch) {
|
|
153
|
+
inlineMetadata.push(`Type: ${typeMatch[1].trim()} `);
|
|
154
|
+
}
|
|
155
|
+
if (assigneeMatch) {
|
|
156
|
+
inlineMetadata.push(`Assignee: ${assigneeMatch[1].trim()} `);
|
|
157
|
+
}
|
|
158
|
+
newContent = [
|
|
159
|
+
...beforeTitle,
|
|
160
|
+
"",
|
|
161
|
+
...inlineMetadata,
|
|
162
|
+
"",
|
|
163
|
+
...afterTitle
|
|
164
|
+
].join("\n");
|
|
165
|
+
wasModified = true;
|
|
166
|
+
}
|
|
167
|
+
if (needsSpaceFix) {
|
|
168
|
+
newContent = newContent.replace(/^(# .+)\n([^#\n])/m, "$1\n\n$2");
|
|
169
|
+
newContent = newContent.replace(/^(Status|Tipo):\s*(.+?)(\s*)$/im, (_, key, value) => `${key}: ${value.trim()} `);
|
|
170
|
+
newContent = newContent.replace(/^(Type|Tipo):\s*(.+?)(\s*)$/im, (_, key, value) => `${key}: ${value.trim()} `);
|
|
171
|
+
newContent = newContent.replace(/^(Assignee|Responsável):\s*(.+?)(\s*)$/im, (_, key, value) => `${key}: ${value.trim()} `);
|
|
172
|
+
wasModified = true;
|
|
173
|
+
}
|
|
174
|
+
if (wasModified) {
|
|
175
|
+
const finalContent = newContent.replace(/\n{3,}/g, "\n\n").trim() + "\n";
|
|
176
|
+
await writeFile(filePath, finalContent, "utf-8");
|
|
177
|
+
return true;
|
|
101
178
|
}
|
|
102
|
-
|
|
103
|
-
const finalContent = fixed.replace(/\n{3,}/g, "\n\n").trim() + "\n";
|
|
104
|
-
await writeFile(filePath, finalContent, "utf-8");
|
|
105
|
-
return true;
|
|
179
|
+
return false;
|
|
106
180
|
} catch (error2) {
|
|
107
181
|
console.error(`Failed to fix ${filePath}:`, error2);
|
|
108
182
|
return false;
|
|
@@ -113,9 +187,13 @@ async function validateTaskFile(filePath) {
|
|
|
113
187
|
try {
|
|
114
188
|
const content = await readFile(filePath, "utf-8");
|
|
115
189
|
const lines = content.split("\n");
|
|
190
|
+
const locale = detectLocale(content);
|
|
191
|
+
const i18n = getI18n(locale);
|
|
116
192
|
const hasTitleSection = lines.some((line) => line.trim().startsWith("# "));
|
|
117
|
-
const
|
|
118
|
-
const
|
|
193
|
+
const inlineStatusPattern = new RegExp(`^(?:Status|${i18n.status}):\\s*.+$`, "im");
|
|
194
|
+
const sectionStatusPattern = new RegExp(`##\\s*(?:Status|${i18n.status})`, "i");
|
|
195
|
+
const hasInlineStatus = inlineStatusPattern.test(content);
|
|
196
|
+
const hasSectionStatus = sectionStatusPattern.test(content);
|
|
119
197
|
const hasDescriptionSection = content.includes("## Description") || content.includes("## Descri\xE7\xE3o");
|
|
120
198
|
if (!hasTitleSection) {
|
|
121
199
|
issues.push({
|
|
@@ -126,38 +204,43 @@ async function validateTaskFile(filePath) {
|
|
|
126
204
|
suggestion: "Add a level-1 heading at the start: # Your Task Title"
|
|
127
205
|
});
|
|
128
206
|
}
|
|
129
|
-
if (
|
|
130
|
-
const statusLineIdx = lines.findIndex(
|
|
131
|
-
(line) => /^Status:/i.test(line.trim())
|
|
132
|
-
);
|
|
207
|
+
if (hasSectionStatus) {
|
|
208
|
+
const statusLineIdx = lines.findIndex((line) => sectionStatusPattern.test(line.trim()));
|
|
133
209
|
issues.push({
|
|
134
210
|
file: filePath,
|
|
135
211
|
line: statusLineIdx >= 0 ? statusLineIdx + 1 : void 0,
|
|
136
|
-
message: '
|
|
212
|
+
message: 'Section-based metadata ("## Status") is not allowed. Use inline format instead.',
|
|
137
213
|
severity: "error",
|
|
138
|
-
suggestion:
|
|
214
|
+
suggestion: `Replace section with inline metadata:
|
|
215
|
+
${i18n.status}: <todo|in-progress|done>`
|
|
139
216
|
});
|
|
140
217
|
}
|
|
141
|
-
if (!
|
|
218
|
+
if (!hasInlineStatus) {
|
|
142
219
|
issues.push({
|
|
143
220
|
file: filePath,
|
|
144
|
-
message: "Task file must have a
|
|
221
|
+
message: "Task file must have a Status field",
|
|
145
222
|
severity: "error",
|
|
146
|
-
suggestion:
|
|
223
|
+
suggestion: `Add inline metadata after title:
|
|
224
|
+
${i18n.status}: <todo|in-progress|done>`
|
|
147
225
|
});
|
|
148
226
|
} else {
|
|
149
|
-
const statusMatch = content.match(
|
|
150
|
-
const statusValue = statusMatch ? statusMatch[1]
|
|
151
|
-
if (![
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
227
|
+
const statusMatch = content.match(inlineStatusPattern);
|
|
228
|
+
const statusValue = statusMatch ? statusMatch[0].split(":")[1]?.trim().toLowerCase() || "" : "";
|
|
229
|
+
if (![
|
|
230
|
+
"todo",
|
|
231
|
+
"in-progress",
|
|
232
|
+
"done",
|
|
233
|
+
"pending",
|
|
234
|
+
"blocked",
|
|
235
|
+
"canceled"
|
|
236
|
+
].includes(statusValue)) {
|
|
237
|
+
const statusLineIdx = lines.findIndex((line) => inlineStatusPattern.test(line.trim()));
|
|
155
238
|
issues.push({
|
|
156
239
|
file: filePath,
|
|
157
|
-
line: statusLineIdx >= 0 ? statusLineIdx +
|
|
158
|
-
message: "Status must be one of: todo, in-progress, done, pending",
|
|
240
|
+
line: statusLineIdx >= 0 ? statusLineIdx + 1 : void 0,
|
|
241
|
+
message: "Status must be one of: todo, in-progress, done, pending, blocked, canceled",
|
|
159
242
|
severity: "error",
|
|
160
|
-
suggestion: "Set status to: todo, in-progress, done, or
|
|
243
|
+
suggestion: "Set status to: todo, in-progress, done, pending, blocked, or canceled"
|
|
161
244
|
});
|
|
162
245
|
}
|
|
163
246
|
}
|
|
@@ -198,15 +281,9 @@ async function validateTaskFile(filePath) {
|
|
|
198
281
|
return issues;
|
|
199
282
|
}
|
|
200
283
|
function createLintResult(allIssues) {
|
|
201
|
-
const errorCount = allIssues.filter(
|
|
202
|
-
|
|
203
|
-
).length;
|
|
204
|
-
const warningCount = allIssues.filter(
|
|
205
|
-
(issue) => issue.severity === "warning"
|
|
206
|
-
).length;
|
|
207
|
-
const infoCount = allIssues.filter(
|
|
208
|
-
(issue) => issue.severity === "info"
|
|
209
|
-
).length;
|
|
284
|
+
const errorCount = allIssues.filter((issue) => issue.severity === "error").length;
|
|
285
|
+
const warningCount = allIssues.filter((issue) => issue.severity === "warning").length;
|
|
286
|
+
const infoCount = allIssues.filter((issue) => issue.severity === "info").length;
|
|
210
287
|
return {
|
|
211
288
|
valid: errorCount === 0,
|
|
212
289
|
issues: allIssues,
|
|
@@ -219,6 +296,7 @@ var init_task_validator = __esm({
|
|
|
219
296
|
"../fs-task-provider/dist/task-validator.js"() {
|
|
220
297
|
"use strict";
|
|
221
298
|
init_esm_shims();
|
|
299
|
+
init_i18n();
|
|
222
300
|
}
|
|
223
301
|
});
|
|
224
302
|
|
|
@@ -7063,47 +7141,10 @@ init_esm_shims();
|
|
|
7063
7141
|
|
|
7064
7142
|
// ../fs-task-provider/dist/fs-task-provider.js
|
|
7065
7143
|
init_esm_shims();
|
|
7144
|
+
init_i18n();
|
|
7145
|
+
init_task_validator();
|
|
7066
7146
|
import { promises as fs } from "fs";
|
|
7067
7147
|
import path2 from "path";
|
|
7068
|
-
|
|
7069
|
-
// ../fs-task-provider/dist/i18n.js
|
|
7070
|
-
init_esm_shims();
|
|
7071
|
-
var i18nConfig = {
|
|
7072
|
-
"en-US": {
|
|
7073
|
-
status: "Status",
|
|
7074
|
-
type: "Type",
|
|
7075
|
-
assignee: "Assignee",
|
|
7076
|
-
description: "Description",
|
|
7077
|
-
tasks: "Tasks",
|
|
7078
|
-
notes: "Notes",
|
|
7079
|
-
defaultAssignee: "To be defined",
|
|
7080
|
-
descriptionPlaceholder: "Add task description here...",
|
|
7081
|
-
notesPlaceholder: "Add any relevant notes or links here."
|
|
7082
|
-
},
|
|
7083
|
-
"pt-BR": {
|
|
7084
|
-
status: "Status",
|
|
7085
|
-
type: "Tipo",
|
|
7086
|
-
assignee: "Respons\xE1vel",
|
|
7087
|
-
description: "Descri\xE7\xE3o",
|
|
7088
|
-
tasks: "Tarefas",
|
|
7089
|
-
notes: "Notas",
|
|
7090
|
-
defaultAssignee: "A definir",
|
|
7091
|
-
descriptionPlaceholder: "Adicione a descri\xE7\xE3o da tarefa aqui...",
|
|
7092
|
-
notesPlaceholder: "Adicione notas ou links relevantes aqui."
|
|
7093
|
-
}
|
|
7094
|
-
};
|
|
7095
|
-
function getI18n(locale = "en-US") {
|
|
7096
|
-
return i18nConfig[locale];
|
|
7097
|
-
}
|
|
7098
|
-
function detectLocale(content) {
|
|
7099
|
-
if (content.includes("## Descri\xE7\xE3o") || content.includes("## Tipo") || content.includes("## Respons\xE1vel") || content.includes("## Tarefas")) {
|
|
7100
|
-
return "pt-BR";
|
|
7101
|
-
}
|
|
7102
|
-
return "en-US";
|
|
7103
|
-
}
|
|
7104
|
-
|
|
7105
|
-
// ../fs-task-provider/dist/fs-task-provider.js
|
|
7106
|
-
init_task_validator();
|
|
7107
7148
|
var FileSystemTaskProvider = class {
|
|
7108
7149
|
tasksDirectory;
|
|
7109
7150
|
userRegistry;
|
|
@@ -7115,9 +7156,7 @@ var FileSystemTaskProvider = class {
|
|
|
7115
7156
|
}
|
|
7116
7157
|
async findTask(taskId) {
|
|
7117
7158
|
const files = await fs.readdir(this.tasksDirectory);
|
|
7118
|
-
const taskFile = files.find(
|
|
7119
|
-
(file) => file.startsWith(`task-${taskId}-`) && file.endsWith(".md")
|
|
7120
|
-
);
|
|
7159
|
+
const taskFile = files.find((file) => file.startsWith(`task-${taskId}-`) && file.endsWith(".md"));
|
|
7121
7160
|
if (!taskFile) {
|
|
7122
7161
|
return void 0;
|
|
7123
7162
|
}
|
|
@@ -7127,26 +7166,26 @@ var FileSystemTaskProvider = class {
|
|
|
7127
7166
|
const title = titleMatch ? titleMatch[1].trim() : "Untitled";
|
|
7128
7167
|
const contentLocale = detectLocale(content);
|
|
7129
7168
|
const i18n = getI18n(contentLocale);
|
|
7130
|
-
const
|
|
7169
|
+
const extractInline = (name, localizedName) => {
|
|
7131
7170
|
const names = localizedName && localizedName !== name ? [localizedName, name] : [name];
|
|
7132
7171
|
for (const n of names) {
|
|
7133
|
-
const
|
|
7172
|
+
const escapedName = n.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
7173
|
+
const rx = new RegExp(`^${escapedName}:\\s*(.+)$`, "im");
|
|
7134
7174
|
const m = content.match(rx);
|
|
7135
|
-
if (m)
|
|
7175
|
+
if (m)
|
|
7176
|
+
return m[1].trim();
|
|
7136
7177
|
}
|
|
7137
7178
|
return null;
|
|
7138
7179
|
};
|
|
7139
|
-
const statusMatch =
|
|
7140
|
-
const typeMatch =
|
|
7141
|
-
const assigneeMatch =
|
|
7180
|
+
const statusMatch = extractInline("Status", i18n.status);
|
|
7181
|
+
const typeMatch = extractInline("Type", i18n.type);
|
|
7182
|
+
const assigneeMatch = extractInline("Assignee", i18n.assignee);
|
|
7142
7183
|
let assignee;
|
|
7143
7184
|
if (assigneeMatch) {
|
|
7144
7185
|
const assigneeValue = assigneeMatch.trim();
|
|
7145
7186
|
assignee = this.userRegistry.resolveUser(assigneeValue);
|
|
7146
7187
|
if (!assignee) {
|
|
7147
|
-
console.warn(
|
|
7148
|
-
`[FS Provider] User "${assigneeValue}" not found in registry, creating temporary user`
|
|
7149
|
-
);
|
|
7188
|
+
console.warn(`[FS Provider] User "${assigneeValue}" not found in registry, creating temporary user`);
|
|
7150
7189
|
assignee = this.userRegistry.createTemporaryUser(assigneeValue);
|
|
7151
7190
|
}
|
|
7152
7191
|
}
|
|
@@ -7164,36 +7203,24 @@ var FileSystemTaskProvider = class {
|
|
|
7164
7203
|
}
|
|
7165
7204
|
async updateTask(task) {
|
|
7166
7205
|
const currentContent = await fs.readFile(task.filePath, "utf-8");
|
|
7167
|
-
const
|
|
7168
|
-
|
|
7169
|
-
);
|
|
7170
|
-
if (hasInlineMetadata) {
|
|
7206
|
+
const hasSectionMetadata = /##\s*(Status|Type|Assignee)/i.test(currentContent);
|
|
7207
|
+
if (hasSectionMetadata) {
|
|
7171
7208
|
const { fixTaskFile: fixTaskFile2 } = await Promise.resolve().then(() => (init_task_validator(), task_validator_exports));
|
|
7172
7209
|
await fixTaskFile2(task.filePath);
|
|
7173
7210
|
}
|
|
7174
7211
|
const content = await fs.readFile(task.filePath, "utf-8");
|
|
7175
7212
|
let updatedContent;
|
|
7176
|
-
if (
|
|
7177
|
-
updatedContent = content.replace(
|
|
7178
|
-
/(##\s*Status\s*\n\s*)([^\n\r]*)/i,
|
|
7179
|
-
`$1${task.status}`
|
|
7180
|
-
);
|
|
7213
|
+
if (/^Status:\s*.+$/im.test(content)) {
|
|
7214
|
+
updatedContent = content.replace(/^Status:\s*.+$/im, `Status: ${task.status} `);
|
|
7181
7215
|
} else {
|
|
7182
|
-
updatedContent = content.replace(
|
|
7183
|
-
|
|
7184
|
-
`$1
|
|
7185
|
-
## Status
|
|
7186
|
-
${task.status}
|
|
7187
|
-
`
|
|
7188
|
-
);
|
|
7216
|
+
updatedContent = content.replace(/(^#.*\n)/, `$1Status: ${task.status}
|
|
7217
|
+
`);
|
|
7189
7218
|
}
|
|
7190
7219
|
await fs.writeFile(task.filePath, updatedContent, "utf-8");
|
|
7191
7220
|
}
|
|
7192
7221
|
async getAllTasks() {
|
|
7193
7222
|
const files = await fs.readdir(this.tasksDirectory);
|
|
7194
|
-
const taskFiles = files.filter(
|
|
7195
|
-
(file) => file.startsWith("task-") && file.endsWith(".md")
|
|
7196
|
-
);
|
|
7223
|
+
const taskFiles = files.filter((file) => file.startsWith("task-") && file.endsWith(".md"));
|
|
7197
7224
|
const tasks = [];
|
|
7198
7225
|
for (const file of taskFiles) {
|
|
7199
7226
|
const filePath = path2.join(this.tasksDirectory, file);
|
|
@@ -7204,18 +7231,20 @@ ${task.status}
|
|
|
7204
7231
|
const title = titleMatch ? titleMatch[1].trim() : "Untitled";
|
|
7205
7232
|
const contentLocale = detectLocale(content);
|
|
7206
7233
|
const i18n = getI18n(contentLocale);
|
|
7207
|
-
const
|
|
7234
|
+
const extractInline = (name, localizedName) => {
|
|
7208
7235
|
const names = localizedName && localizedName !== name ? [localizedName, name] : [name];
|
|
7209
7236
|
for (const n of names) {
|
|
7210
|
-
const
|
|
7237
|
+
const escapedName = n.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
7238
|
+
const rx = new RegExp(`^${escapedName}:\\s*(.+)$`, "im");
|
|
7211
7239
|
const m = content.match(rx);
|
|
7212
|
-
if (m)
|
|
7240
|
+
if (m)
|
|
7241
|
+
return m[1].trim();
|
|
7213
7242
|
}
|
|
7214
7243
|
return null;
|
|
7215
7244
|
};
|
|
7216
|
-
const statusMatch =
|
|
7217
|
-
const typeMatch =
|
|
7218
|
-
const assigneeMatch =
|
|
7245
|
+
const statusMatch = extractInline("Status", i18n.status);
|
|
7246
|
+
const typeMatch = extractInline("Type", i18n.type);
|
|
7247
|
+
const assigneeMatch = extractInline("Assignee", i18n.assignee);
|
|
7219
7248
|
let assignee;
|
|
7220
7249
|
if (assigneeMatch) {
|
|
7221
7250
|
const assigneeValue = assigneeMatch.trim();
|
|
@@ -7239,8 +7268,13 @@ ${task.status}
|
|
|
7239
7268
|
return tasks;
|
|
7240
7269
|
}
|
|
7241
7270
|
async createTask(options) {
|
|
7242
|
-
const i18n = getI18n(this.locale);
|
|
7243
7271
|
const allTasks = await this.getAllTasks();
|
|
7272
|
+
let detectedLocale = this.locale;
|
|
7273
|
+
if (allTasks.length > 0) {
|
|
7274
|
+
const lastTask = allTasks[allTasks.length - 1];
|
|
7275
|
+
detectedLocale = detectLocale(lastTask.content);
|
|
7276
|
+
}
|
|
7277
|
+
const i18n = getI18n(detectedLocale);
|
|
7244
7278
|
const taskNumbers = allTasks.map((task2) => {
|
|
7245
7279
|
const match = task2.id.match(/^(\d+)$/);
|
|
7246
7280
|
return match ? parseInt(match[1], 10) : 0;
|
|
@@ -7284,30 +7318,19 @@ ${task.status}
|
|
|
7284
7318
|
const { id, title, type, description, assignee, i18n } = data;
|
|
7285
7319
|
return `# \u{1F9E9} Task ${id} \u2014 ${title}
|
|
7286
7320
|
|
|
7287
|
-
|
|
7288
|
-
|
|
7289
|
-
|
|
7290
|
-
|
|
7291
|
-
## ${i18n.type}
|
|
7292
|
-
|
|
7293
|
-
${type}
|
|
7294
|
-
|
|
7295
|
-
## ${i18n.assignee}
|
|
7296
|
-
|
|
7297
|
-
${assignee}
|
|
7321
|
+
${i18n.status}: pending
|
|
7322
|
+
${i18n.type}: ${type}
|
|
7323
|
+
${i18n.assignee}: ${assignee}
|
|
7298
7324
|
|
|
7299
7325
|
## ${i18n.description}
|
|
7300
|
-
|
|
7301
7326
|
${description || i18n.descriptionPlaceholder}
|
|
7302
7327
|
|
|
7303
7328
|
## ${i18n.tasks}
|
|
7304
|
-
|
|
7305
7329
|
- [ ] Task 1
|
|
7306
7330
|
- [ ] Task 2
|
|
7307
7331
|
- [ ] Task 3
|
|
7308
7332
|
|
|
7309
7333
|
## ${i18n.notes}
|
|
7310
|
-
|
|
7311
7334
|
${i18n.notesPlaceholder}
|
|
7312
7335
|
`;
|
|
7313
7336
|
}
|
|
@@ -7335,6 +7358,9 @@ ${i18n.notesPlaceholder}
|
|
|
7335
7358
|
}
|
|
7336
7359
|
};
|
|
7337
7360
|
|
|
7361
|
+
// ../fs-task-provider/dist/index.js
|
|
7362
|
+
init_i18n();
|
|
7363
|
+
|
|
7338
7364
|
// ../fs-task-provider/dist/user-registry.js
|
|
7339
7365
|
init_esm_shims();
|
|
7340
7366
|
import { promises as fs2 } from "fs";
|
|
@@ -7834,38 +7860,41 @@ init_esm_shims();
|
|
|
7834
7860
|
// ../utils/dist/security.js
|
|
7835
7861
|
init_esm_shims();
|
|
7836
7862
|
import { z } from "zod";
|
|
7837
|
-
var HostSchema = z.string().refine(
|
|
7838
|
-
|
|
7839
|
-
return false;
|
|
7840
|
-
|
|
7841
|
-
|
|
7842
|
-
|
|
7843
|
-
|
|
7844
|
-
|
|
7845
|
-
|
|
7846
|
-
|
|
7847
|
-
|
|
7848
|
-
|
|
7849
|
-
|
|
7850
|
-
})
|
|
7863
|
+
var HostSchema = z.string().refine(
|
|
7864
|
+
(host) => {
|
|
7865
|
+
if (!host || host.length === 0) return false;
|
|
7866
|
+
if (host === "localhost") return true;
|
|
7867
|
+
const parts = host.split(".");
|
|
7868
|
+
const allNumeric = parts.every((p) => /^\d+$/.test(p));
|
|
7869
|
+
if (allNumeric) {
|
|
7870
|
+
if (parts.length !== 4) return false;
|
|
7871
|
+
return parts.every((part) => {
|
|
7872
|
+
const num = parseInt(part, 10);
|
|
7873
|
+
return !isNaN(num) && num >= 0 && num <= 255 && part === num.toString();
|
|
7874
|
+
});
|
|
7875
|
+
}
|
|
7876
|
+
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])?)*$/;
|
|
7877
|
+
return hostnameRegex.test(host);
|
|
7878
|
+
},
|
|
7879
|
+
{
|
|
7880
|
+
message: "Invalid host. Must be localhost, a valid IPv4 address, or hostname."
|
|
7851
7881
|
}
|
|
7852
|
-
|
|
7853
|
-
return hostnameRegex.test(host);
|
|
7854
|
-
}, {
|
|
7855
|
-
message: "Invalid host. Must be localhost, a valid IPv4 address, or hostname."
|
|
7856
|
-
});
|
|
7882
|
+
);
|
|
7857
7883
|
var PortSchema = z.union([
|
|
7858
7884
|
z.number().int().min(1).max(65535),
|
|
7859
7885
|
z.string().regex(/^\d+$/).transform((val) => parseInt(val, 10)).pipe(z.number().int().min(1).max(65535))
|
|
7860
7886
|
]);
|
|
7861
|
-
var WebSocketUrlSchema = z.string().refine(
|
|
7862
|
-
|
|
7863
|
-
|
|
7864
|
-
|
|
7865
|
-
|
|
7866
|
-
|
|
7867
|
-
|
|
7868
|
-
}
|
|
7887
|
+
var WebSocketUrlSchema = z.string().refine(
|
|
7888
|
+
(url) => {
|
|
7889
|
+
try {
|
|
7890
|
+
const parsed = new URL(url);
|
|
7891
|
+
return parsed.protocol === "ws:" || parsed.protocol === "wss:";
|
|
7892
|
+
} catch {
|
|
7893
|
+
return false;
|
|
7894
|
+
}
|
|
7895
|
+
},
|
|
7896
|
+
{ message: "Invalid WebSocket URL. Must use ws:// or wss:// protocol." }
|
|
7897
|
+
);
|
|
7869
7898
|
var TaskIdSchema = z.string().min(1).max(100).regex(/^[a-zA-Z0-9_-]+$/, {
|
|
7870
7899
|
message: "Task ID must contain only alphanumeric characters, hyphens, and underscores."
|
|
7871
7900
|
});
|
|
@@ -7873,23 +7902,25 @@ var UserIdSchema = z.string().min(1).max(100).regex(/^[a-zA-Z0-9._-]+$/, {
|
|
|
7873
7902
|
message: "User ID must contain only alphanumeric characters, dots, hyphens, and underscores."
|
|
7874
7903
|
});
|
|
7875
7904
|
var EmailSchema = z.string().email().max(254);
|
|
7876
|
-
var SafePathSchema = z.string().refine(
|
|
7877
|
-
|
|
7878
|
-
return false;
|
|
7879
|
-
|
|
7880
|
-
|
|
7881
|
-
|
|
7882
|
-
|
|
7883
|
-
|
|
7884
|
-
|
|
7885
|
-
|
|
7886
|
-
|
|
7887
|
-
|
|
7888
|
-
|
|
7889
|
-
|
|
7890
|
-
},
|
|
7891
|
-
|
|
7892
|
-
|
|
7905
|
+
var SafePathSchema = z.string().refine(
|
|
7906
|
+
(filePath) => {
|
|
7907
|
+
if (!filePath || filePath.length === 0) return false;
|
|
7908
|
+
const dangerousPatterns = [
|
|
7909
|
+
/\.\./,
|
|
7910
|
+
// Parent directory (..)
|
|
7911
|
+
/~\//,
|
|
7912
|
+
// Home directory
|
|
7913
|
+
/^\//,
|
|
7914
|
+
// Absolute path
|
|
7915
|
+
/^[A-Za-z]:\\/
|
|
7916
|
+
// Windows absolute path
|
|
7917
|
+
];
|
|
7918
|
+
return !dangerousPatterns.some((pattern) => pattern.test(filePath));
|
|
7919
|
+
},
|
|
7920
|
+
{
|
|
7921
|
+
message: "Invalid path. Must be a relative path without traversal patterns."
|
|
7922
|
+
}
|
|
7923
|
+
);
|
|
7893
7924
|
var DashboardOptionsSchema = z.object({
|
|
7894
7925
|
host: HostSchema.optional(),
|
|
7895
7926
|
port: PortSchema.optional(),
|
|
@@ -8666,8 +8697,15 @@ async function setupFileSystemProvider(cwd) {
|
|
|
8666
8697
|
info(`${colors2.highlight("TASKS/")} directory already exists`);
|
|
8667
8698
|
success("\u2713 Directory is ready to use");
|
|
8668
8699
|
}
|
|
8669
|
-
const
|
|
8670
|
-
|
|
8700
|
+
const fs3 = __require("fs");
|
|
8701
|
+
const existingTask001 = fs3.readdirSync(tasksDir).find(
|
|
8702
|
+
(file) => file.startsWith("task-001-") && file.endsWith(".md")
|
|
8703
|
+
);
|
|
8704
|
+
if (existingTask001) {
|
|
8705
|
+
info(`Sample task already exists: ${colors2.highlight(existingTask001)}`);
|
|
8706
|
+
info("Skipping sample task creation (users already know the pattern)");
|
|
8707
|
+
} else {
|
|
8708
|
+
const sampleTaskFile = join(tasksDir, "task-001-setup-project.md");
|
|
8671
8709
|
info("Creating sample task...");
|
|
8672
8710
|
const sampleTask = `# Task 001 \u2014 Setup Project
|
|
8673
8711
|
|
|
@@ -8701,303 +8739,59 @@ You can edit or delete this file. Use \`taskin list\` to see all tasks.
|
|
|
8701
8739
|
|
|
8702
8740
|
// src/commands/lint.ts
|
|
8703
8741
|
init_esm_shims();
|
|
8704
|
-
import { join as join3 } from "path";
|
|
8705
|
-
|
|
8706
|
-
// src/lib/file-system-task-linter/index.ts
|
|
8707
|
-
init_esm_shims();
|
|
8708
|
-
|
|
8709
|
-
// src/lib/file-system-task-linter/file-system-task-linter.ts
|
|
8710
|
-
init_esm_shims();
|
|
8711
8742
|
import chalk4 from "chalk";
|
|
8712
|
-
import { readdir, readFile as readFile2 } from "fs/promises";
|
|
8713
8743
|
import { join as join2 } from "path";
|
|
8714
|
-
var
|
|
8715
|
-
|
|
8716
|
-
|
|
8717
|
-
|
|
8718
|
-
|
|
8719
|
-
|
|
8720
|
-
|
|
8721
|
-
|
|
8722
|
-
|
|
8723
|
-
|
|
8724
|
-
|
|
8725
|
-
|
|
8726
|
-
if (!pattern.test(fileName)) {
|
|
8727
|
-
return {
|
|
8728
|
-
file: fileName,
|
|
8729
|
-
message: "Invalid filename. Expected: task-NNN-kebab-case-title.md",
|
|
8730
|
-
severity: "error"
|
|
8731
|
-
};
|
|
8744
|
+
var lintCommand = defineCommand({
|
|
8745
|
+
name: "lint",
|
|
8746
|
+
description: "\u{1F50D} Validate task markdown files",
|
|
8747
|
+
options: [
|
|
8748
|
+
{
|
|
8749
|
+
flags: "-p, --path <directory>",
|
|
8750
|
+
description: "Path to TASKS directory",
|
|
8751
|
+
defaultValue: "TASKS"
|
|
8752
|
+
},
|
|
8753
|
+
{
|
|
8754
|
+
flags: "-f, --fix",
|
|
8755
|
+
description: "Automatically fix task file format issues"
|
|
8732
8756
|
}
|
|
8733
|
-
|
|
8757
|
+
],
|
|
8758
|
+
handler: async (options) => {
|
|
8759
|
+
await executeLint(options);
|
|
8734
8760
|
}
|
|
8735
|
-
|
|
8736
|
-
|
|
8737
|
-
|
|
8738
|
-
|
|
8739
|
-
|
|
8740
|
-
|
|
8741
|
-
|
|
8742
|
-
|
|
8743
|
-
|
|
8744
|
-
};
|
|
8761
|
+
});
|
|
8762
|
+
async function executeLint(options) {
|
|
8763
|
+
const tasksDir = options.path || join2(process.cwd(), "TASKS");
|
|
8764
|
+
if (options.fix) {
|
|
8765
|
+
console.log(`\u{1F527} Fixing task files in: ${tasksDir}
|
|
8766
|
+
`);
|
|
8767
|
+
} else {
|
|
8768
|
+
console.log(`\u{1F4CB} Linting task files in: ${tasksDir}
|
|
8769
|
+
`);
|
|
8745
8770
|
}
|
|
8746
|
-
|
|
8747
|
-
|
|
8748
|
-
|
|
8749
|
-
|
|
8750
|
-
|
|
8751
|
-
|
|
8752
|
-
|
|
8753
|
-
|
|
8754
|
-
|
|
8755
|
-
|
|
8756
|
-
|
|
8757
|
-
|
|
8758
|
-
|
|
8759
|
-
|
|
8760
|
-
|
|
8761
|
-
severity: "error"
|
|
8762
|
-
});
|
|
8763
|
-
}
|
|
8764
|
-
if (!metadata.type) {
|
|
8765
|
-
errors.push({
|
|
8766
|
-
file: filePath,
|
|
8767
|
-
message: "Missing required metadata: Type",
|
|
8768
|
-
severity: "error"
|
|
8769
|
-
});
|
|
8770
|
-
} else if (!VALID_TYPES.includes(metadata.type)) {
|
|
8771
|
-
errors.push({
|
|
8772
|
-
file: filePath,
|
|
8773
|
-
message: `Invalid type "${metadata.type}". Valid: ${VALID_TYPES.join(", ")}`,
|
|
8774
|
-
severity: "error"
|
|
8775
|
-
});
|
|
8776
|
-
}
|
|
8777
|
-
if (!metadata.assignee) {
|
|
8778
|
-
errors.push({
|
|
8779
|
-
file: filePath,
|
|
8780
|
-
message: "Missing recommended metadata: Assignee",
|
|
8781
|
-
severity: "warning"
|
|
8782
|
-
});
|
|
8771
|
+
const userRegistry = new UserRegistry({
|
|
8772
|
+
taskinDir: join2(process.cwd(), ".taskin")
|
|
8773
|
+
});
|
|
8774
|
+
await userRegistry.load();
|
|
8775
|
+
const provider = new FileSystemTaskProvider(tasksDir, userRegistry);
|
|
8776
|
+
const result = await provider.lint(options.fix);
|
|
8777
|
+
if (result.valid) {
|
|
8778
|
+
console.log(chalk4.green(`\u2705 All task files are valid!
|
|
8779
|
+
`));
|
|
8780
|
+
} else {
|
|
8781
|
+
console.log(chalk4.red(`
|
|
8782
|
+
\u274C Found ${result.issues.length} issue(s):
|
|
8783
|
+
`));
|
|
8784
|
+
for (const issue of result.issues) {
|
|
8785
|
+
console.log(chalk4.yellow(` ${issue.file}: ${issue.message}`));
|
|
8783
8786
|
}
|
|
8784
|
-
|
|
8787
|
+
console.log();
|
|
8785
8788
|
}
|
|
8786
|
-
|
|
8787
|
-
|
|
8788
|
-
|
|
8789
|
-
|
|
8790
|
-
|
|
8791
|
-
|
|
8792
|
-
"error",
|
|
8793
|
-
1
|
|
8794
|
-
);
|
|
8795
|
-
}
|
|
8796
|
-
const h1Pattern = /^#\s+(?:🧩\s+)?Task\s+\d{2,3}\s+[—-]\s+.+$/i;
|
|
8797
|
-
if (lines[0] && !h1Pattern.test(lines[0])) {
|
|
8798
|
-
this.addError(
|
|
8799
|
-
filename,
|
|
8800
|
-
'H1 heading must follow format: "# Task NNN \u2014 Title"',
|
|
8801
|
-
"error",
|
|
8802
|
-
1
|
|
8803
|
-
);
|
|
8804
|
-
}
|
|
8805
|
-
const metadata = this.extractMetadata(content);
|
|
8806
|
-
const metadataErrors = this.validateMetadata(metadata, filename);
|
|
8807
|
-
metadataErrors.forEach((error2) => {
|
|
8808
|
-
this.addError(error2.file, error2.message, error2.severity, error2.line);
|
|
8809
|
-
});
|
|
8810
|
-
const h2Sections = content.match(/^##\s+.+$/gm);
|
|
8811
|
-
if (!h2Sections || h2Sections.length === 0) {
|
|
8812
|
-
this.addError(
|
|
8813
|
-
filename,
|
|
8814
|
-
"Task should have at least one section (## heading)",
|
|
8815
|
-
"warning"
|
|
8816
|
-
);
|
|
8817
|
-
}
|
|
8818
|
-
const firstH2Index = content.indexOf("\n##");
|
|
8819
|
-
if (firstH2Index > -1) {
|
|
8820
|
-
const afterH2 = content.substring(firstH2Index);
|
|
8821
|
-
if (afterH2.match(/^Status:/im) || afterH2.match(/^Type:/im) || afterH2.match(/^Assignee:/im)) {
|
|
8822
|
-
this.addError(
|
|
8823
|
-
filename,
|
|
8824
|
-
"Metadata must be placed BEFORE the first ## section",
|
|
8825
|
-
"error"
|
|
8826
|
-
);
|
|
8827
|
-
}
|
|
8828
|
-
}
|
|
8829
|
-
}
|
|
8830
|
-
/**
|
|
8831
|
-
* Lint a single task file (FileSystem-specific)
|
|
8832
|
-
*/
|
|
8833
|
-
async lintFile(filePath) {
|
|
8834
|
-
const errors = [];
|
|
8835
|
-
const fileName = filePath.split("/").pop() || filePath;
|
|
8836
|
-
const fileNameError = this.validateFileName(fileName);
|
|
8837
|
-
if (fileNameError) {
|
|
8838
|
-
errors.push(fileNameError);
|
|
8839
|
-
}
|
|
8840
|
-
try {
|
|
8841
|
-
const content = await readFile2(filePath, "utf-8");
|
|
8842
|
-
this.errors = [];
|
|
8843
|
-
this.validateContent(fileName, content);
|
|
8844
|
-
errors.push(...this.errors);
|
|
8845
|
-
} catch (error2) {
|
|
8846
|
-
errors.push({
|
|
8847
|
-
file: fileName,
|
|
8848
|
-
message: `Failed to read file: ${error2}`,
|
|
8849
|
-
severity: "error"
|
|
8850
|
-
});
|
|
8851
|
-
}
|
|
8852
|
-
return errors;
|
|
8853
|
-
}
|
|
8854
|
-
/**
|
|
8855
|
-
* Lint all task files in the specified directory (FileSystem-specific)
|
|
8856
|
-
*/
|
|
8857
|
-
async lintDirectory(tasksDir) {
|
|
8858
|
-
this.errors = [];
|
|
8859
|
-
try {
|
|
8860
|
-
const files = await readdir(tasksDir);
|
|
8861
|
-
const taskFiles = files.filter((f) => f.endsWith(".md"));
|
|
8862
|
-
for (const file of taskFiles) {
|
|
8863
|
-
const fileNameError = this.validateFileName(file);
|
|
8864
|
-
if (fileNameError) {
|
|
8865
|
-
this.addError(file, fileNameError.message, fileNameError.severity);
|
|
8866
|
-
}
|
|
8867
|
-
const content = await readFile2(join2(tasksDir, file), "utf-8");
|
|
8868
|
-
this.validateContent(file, content);
|
|
8869
|
-
}
|
|
8870
|
-
const errors = this.errors.filter((e) => e.severity === "error");
|
|
8871
|
-
const warnings = this.errors.filter((e) => e.severity === "warning");
|
|
8872
|
-
return {
|
|
8873
|
-
errors,
|
|
8874
|
-
warnings,
|
|
8875
|
-
filesChecked: taskFiles.length,
|
|
8876
|
-
valid: errors.length === 0
|
|
8877
|
-
};
|
|
8878
|
-
} catch (error2) {
|
|
8879
|
-
throw new Error(`Failed to lint tasks: ${error2}`);
|
|
8880
|
-
}
|
|
8881
|
-
}
|
|
8882
|
-
static printResults(result) {
|
|
8883
|
-
if (result.errors.length === 0 && result.warnings.length === 0) {
|
|
8884
|
-
console.log(
|
|
8885
|
-
chalk4.green(`\u2705 All ${result.filesChecked} task files are valid!
|
|
8886
|
-
`)
|
|
8887
|
-
);
|
|
8888
|
-
return;
|
|
8889
|
-
}
|
|
8890
|
-
console.log(
|
|
8891
|
-
chalk4.bold(
|
|
8892
|
-
`
|
|
8893
|
-
\u{1F4CA} Validation Results (${result.filesChecked} files checked):
|
|
8894
|
-
`
|
|
8895
|
-
)
|
|
8896
|
-
);
|
|
8897
|
-
const errorsByFile = /* @__PURE__ */ new Map();
|
|
8898
|
-
[...result.errors, ...result.warnings].forEach((error2) => {
|
|
8899
|
-
if (!errorsByFile.has(error2.file)) {
|
|
8900
|
-
errorsByFile.set(error2.file, []);
|
|
8901
|
-
}
|
|
8902
|
-
errorsByFile.get(error2.file).push(error2);
|
|
8903
|
-
});
|
|
8904
|
-
for (const [file, fileErrors] of errorsByFile) {
|
|
8905
|
-
console.log(chalk4.cyan(`
|
|
8906
|
-
\u{1F4C4} ${file}`));
|
|
8907
|
-
for (const error2 of fileErrors) {
|
|
8908
|
-
const icon = error2.severity === "error" ? chalk4.red("\u274C") : chalk4.yellow("\u26A0\uFE0F");
|
|
8909
|
-
const location = error2.line ? `:${error2.line}` : "";
|
|
8910
|
-
console.log(` ${icon} ${error2.message}${location}`);
|
|
8911
|
-
}
|
|
8912
|
-
}
|
|
8913
|
-
console.log("\n" + "\u2500".repeat(60));
|
|
8914
|
-
console.log(
|
|
8915
|
-
chalk4.bold(
|
|
8916
|
-
`
|
|
8917
|
-
\u{1F4CA} Summary: ${chalk4.red(result.errors.length + " error(s)")}, ${chalk4.yellow(result.warnings.length + " warning(s)")}
|
|
8918
|
-
`
|
|
8919
|
-
)
|
|
8920
|
-
);
|
|
8921
|
-
}
|
|
8922
|
-
};
|
|
8923
|
-
|
|
8924
|
-
// src/lib/file-system-task-linter/file-system-task-linter.mock.ts
|
|
8925
|
-
init_esm_shims();
|
|
8926
|
-
function createMockFileValidationError(overrides) {
|
|
8927
|
-
return {
|
|
8928
|
-
file: "task-001.md",
|
|
8929
|
-
message: "Mock validation error",
|
|
8930
|
-
severity: "error",
|
|
8931
|
-
...overrides
|
|
8932
|
-
};
|
|
8933
|
-
}
|
|
8934
|
-
var MOCK_INVALID_LINT_RESULT = {
|
|
8935
|
-
errors: [
|
|
8936
|
-
createMockFileValidationError({
|
|
8937
|
-
file: "task-001.md",
|
|
8938
|
-
message: 'Invalid status: "invalid"'
|
|
8939
|
-
}),
|
|
8940
|
-
createMockFileValidationError({
|
|
8941
|
-
file: "task-002.md",
|
|
8942
|
-
message: 'Invalid type: "unknown"'
|
|
8943
|
-
})
|
|
8944
|
-
],
|
|
8945
|
-
filesChecked: 2,
|
|
8946
|
-
valid: false,
|
|
8947
|
-
warnings: []
|
|
8948
|
-
};
|
|
8949
|
-
var MOCK_WARNINGS_LINT_RESULT = {
|
|
8950
|
-
errors: [],
|
|
8951
|
-
filesChecked: 3,
|
|
8952
|
-
valid: true,
|
|
8953
|
-
warnings: [
|
|
8954
|
-
createMockFileValidationError({
|
|
8955
|
-
file: "task-003.md",
|
|
8956
|
-
message: "Consider adding assignee",
|
|
8957
|
-
severity: "warning"
|
|
8958
|
-
})
|
|
8959
|
-
]
|
|
8960
|
-
};
|
|
8961
|
-
var MOCK_VALIDATION_ERRORS = [
|
|
8962
|
-
createMockFileValidationError({
|
|
8963
|
-
file: "task-001.md",
|
|
8964
|
-
message: "Invalid status: must be one of [pending, in-progress, done, blocked]",
|
|
8965
|
-
line: 5
|
|
8966
|
-
}),
|
|
8967
|
-
createMockFileValidationError({
|
|
8968
|
-
file: "task-001.md",
|
|
8969
|
-
message: "Invalid type: must be one of [feat, fix, chore, docs, refactor, test]",
|
|
8970
|
-
line: 6
|
|
8971
|
-
})
|
|
8972
|
-
];
|
|
8973
|
-
|
|
8974
|
-
// src/lib/file-system-task-linter/file-system-task-linter.types.ts
|
|
8975
|
-
init_esm_shims();
|
|
8976
|
-
|
|
8977
|
-
// src/commands/lint.ts
|
|
8978
|
-
var lintCommand = defineCommand({
|
|
8979
|
-
name: "lint",
|
|
8980
|
-
description: "\u{1F50D} Validate task markdown files",
|
|
8981
|
-
options: [
|
|
8982
|
-
{
|
|
8983
|
-
flags: "-p, --path <directory>",
|
|
8984
|
-
description: "Path to TASKS directory",
|
|
8985
|
-
defaultValue: "TASKS"
|
|
8986
|
-
}
|
|
8987
|
-
],
|
|
8988
|
-
handler: async (options) => {
|
|
8989
|
-
await executeLint(options);
|
|
8990
|
-
}
|
|
8991
|
-
});
|
|
8992
|
-
async function executeLint(options) {
|
|
8993
|
-
const tasksDir = options.path || join3(process.cwd(), "TASKS");
|
|
8994
|
-
console.log(`\u{1F4CB} Linting task files in: ${tasksDir}
|
|
8995
|
-
`);
|
|
8996
|
-
const linter = new FileSystemTaskLinter();
|
|
8997
|
-
const result = await linter.lintDirectory(tasksDir);
|
|
8998
|
-
FileSystemTaskLinter.printResults(result);
|
|
8999
|
-
if (!result.valid) {
|
|
9000
|
-
process.exit(1);
|
|
8789
|
+
if (!result.valid && !options.fix) {
|
|
8790
|
+
console.log(
|
|
8791
|
+
chalk4.blue(`\u{1F4A1} Run with --fix to automatically fix format issues
|
|
8792
|
+
`)
|
|
8793
|
+
);
|
|
8794
|
+
process.exit(1);
|
|
9001
8795
|
}
|
|
9002
8796
|
}
|
|
9003
8797
|
|
|
@@ -11800,13 +11594,13 @@ function showCustomHelp() {
|
|
|
11800
11594
|
// src/version.ts
|
|
11801
11595
|
init_esm_shims();
|
|
11802
11596
|
import { readFileSync } from "fs";
|
|
11803
|
-
import { dirname, join as
|
|
11597
|
+
import { dirname, join as join3 } from "path";
|
|
11804
11598
|
import { fileURLToPath as fileURLToPath3 } from "url";
|
|
11805
11599
|
var __filename3 = fileURLToPath3(import.meta.url);
|
|
11806
11600
|
var __dirname3 = dirname(__filename3);
|
|
11807
11601
|
function getVersion() {
|
|
11808
11602
|
try {
|
|
11809
|
-
const packageJsonPath =
|
|
11603
|
+
const packageJsonPath = join3(__dirname3, "../package.json");
|
|
11810
11604
|
const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf-8"));
|
|
11811
11605
|
return packageJson.version;
|
|
11812
11606
|
} catch {
|
|
@@ -11818,6 +11612,277 @@ function getVersion() {
|
|
|
11818
11612
|
init_esm_shims();
|
|
11819
11613
|
import { dirname as dirname2, join as join5 } from "path";
|
|
11820
11614
|
|
|
11615
|
+
// src/lib/file-system-task-linter/index.ts
|
|
11616
|
+
init_esm_shims();
|
|
11617
|
+
|
|
11618
|
+
// src/lib/file-system-task-linter/file-system-task-linter.ts
|
|
11619
|
+
init_esm_shims();
|
|
11620
|
+
import chalk6 from "chalk";
|
|
11621
|
+
import { readdir, readFile as readFile2 } from "fs/promises";
|
|
11622
|
+
import { join as join4 } from "path";
|
|
11623
|
+
var VALID_STATUSES = ["pending", "in-progress", "done", "blocked"];
|
|
11624
|
+
var VALID_TYPES = ["feat", "fix", "chore", "docs", "refactor", "test"];
|
|
11625
|
+
var FileSystemTaskLinter = class {
|
|
11626
|
+
errors = [];
|
|
11627
|
+
addError(file, message, severity = "error", line) {
|
|
11628
|
+
this.errors.push({ file, message, severity, line });
|
|
11629
|
+
}
|
|
11630
|
+
/**
|
|
11631
|
+
* Validate task file name format (FileSystem-specific)
|
|
11632
|
+
*/
|
|
11633
|
+
validateFileName(fileName) {
|
|
11634
|
+
const pattern = /^task-\d{2,3}-[a-z0-9-]+\.md$/;
|
|
11635
|
+
if (!pattern.test(fileName)) {
|
|
11636
|
+
return {
|
|
11637
|
+
file: fileName,
|
|
11638
|
+
message: "Invalid filename. Expected: task-NNN-kebab-case-title.md",
|
|
11639
|
+
severity: "error"
|
|
11640
|
+
};
|
|
11641
|
+
}
|
|
11642
|
+
return null;
|
|
11643
|
+
}
|
|
11644
|
+
extractMetadata(content) {
|
|
11645
|
+
const headerSection = content.split(/^##/m)[0];
|
|
11646
|
+
const statusMatch = headerSection.match(/^Status:\s*(.+)$/im);
|
|
11647
|
+
const typeMatch = headerSection.match(/^Type:\s*(.+)$/im);
|
|
11648
|
+
const assigneeMatch = headerSection.match(/^Assignee:\s*(.+)$/im);
|
|
11649
|
+
return {
|
|
11650
|
+
status: statusMatch?.[1]?.trim().toLowerCase(),
|
|
11651
|
+
type: typeMatch?.[1]?.trim().toLowerCase(),
|
|
11652
|
+
assignee: assigneeMatch?.[1]?.trim()
|
|
11653
|
+
};
|
|
11654
|
+
}
|
|
11655
|
+
/**
|
|
11656
|
+
* Validate task metadata (FileSystem-specific)
|
|
11657
|
+
*/
|
|
11658
|
+
validateMetadata(metadata, filePath) {
|
|
11659
|
+
const errors = [];
|
|
11660
|
+
if (!metadata.status) {
|
|
11661
|
+
errors.push({
|
|
11662
|
+
file: filePath,
|
|
11663
|
+
message: "Missing required metadata: Status",
|
|
11664
|
+
severity: "error"
|
|
11665
|
+
});
|
|
11666
|
+
} else if (!VALID_STATUSES.includes(metadata.status)) {
|
|
11667
|
+
errors.push({
|
|
11668
|
+
file: filePath,
|
|
11669
|
+
message: `Invalid status "${metadata.status}". Valid: ${VALID_STATUSES.join(", ")}`,
|
|
11670
|
+
severity: "error"
|
|
11671
|
+
});
|
|
11672
|
+
}
|
|
11673
|
+
if (!metadata.type) {
|
|
11674
|
+
errors.push({
|
|
11675
|
+
file: filePath,
|
|
11676
|
+
message: "Missing required metadata: Type",
|
|
11677
|
+
severity: "error"
|
|
11678
|
+
});
|
|
11679
|
+
} else if (!VALID_TYPES.includes(metadata.type)) {
|
|
11680
|
+
errors.push({
|
|
11681
|
+
file: filePath,
|
|
11682
|
+
message: `Invalid type "${metadata.type}". Valid: ${VALID_TYPES.join(", ")}`,
|
|
11683
|
+
severity: "error"
|
|
11684
|
+
});
|
|
11685
|
+
}
|
|
11686
|
+
if (!metadata.assignee) {
|
|
11687
|
+
errors.push({
|
|
11688
|
+
file: filePath,
|
|
11689
|
+
message: "Missing recommended metadata: Assignee",
|
|
11690
|
+
severity: "warning"
|
|
11691
|
+
});
|
|
11692
|
+
}
|
|
11693
|
+
return errors;
|
|
11694
|
+
}
|
|
11695
|
+
validateContent(filename, content) {
|
|
11696
|
+
const lines = content.split("\n");
|
|
11697
|
+
if (!lines[0]?.trim().startsWith("# ")) {
|
|
11698
|
+
this.addError(
|
|
11699
|
+
filename,
|
|
11700
|
+
"Task file must start with a level 1 heading (# Task NNN \u2014 Title)",
|
|
11701
|
+
"error",
|
|
11702
|
+
1
|
|
11703
|
+
);
|
|
11704
|
+
}
|
|
11705
|
+
const h1Pattern = /^#\s+(?:🧩\s+)?Task\s+\d{2,3}\s+[—-]\s+.+$/i;
|
|
11706
|
+
if (lines[0] && !h1Pattern.test(lines[0])) {
|
|
11707
|
+
this.addError(
|
|
11708
|
+
filename,
|
|
11709
|
+
'H1 heading must follow format: "# Task NNN \u2014 Title"',
|
|
11710
|
+
"error",
|
|
11711
|
+
1
|
|
11712
|
+
);
|
|
11713
|
+
}
|
|
11714
|
+
const metadata = this.extractMetadata(content);
|
|
11715
|
+
const metadataErrors = this.validateMetadata(metadata, filename);
|
|
11716
|
+
metadataErrors.forEach((error2) => {
|
|
11717
|
+
this.addError(error2.file, error2.message, error2.severity, error2.line);
|
|
11718
|
+
});
|
|
11719
|
+
const h2Sections = content.match(/^##\s+.+$/gm);
|
|
11720
|
+
if (!h2Sections || h2Sections.length === 0) {
|
|
11721
|
+
this.addError(
|
|
11722
|
+
filename,
|
|
11723
|
+
"Task should have at least one section (## heading)",
|
|
11724
|
+
"warning"
|
|
11725
|
+
);
|
|
11726
|
+
}
|
|
11727
|
+
const firstH2Index = content.indexOf("\n##");
|
|
11728
|
+
if (firstH2Index > -1) {
|
|
11729
|
+
const afterH2 = content.substring(firstH2Index);
|
|
11730
|
+
if (afterH2.match(/^Status:/im) || afterH2.match(/^Type:/im) || afterH2.match(/^Assignee:/im)) {
|
|
11731
|
+
this.addError(
|
|
11732
|
+
filename,
|
|
11733
|
+
"Metadata must be placed BEFORE the first ## section",
|
|
11734
|
+
"error"
|
|
11735
|
+
);
|
|
11736
|
+
}
|
|
11737
|
+
}
|
|
11738
|
+
}
|
|
11739
|
+
/**
|
|
11740
|
+
* Lint a single task file (FileSystem-specific)
|
|
11741
|
+
*/
|
|
11742
|
+
async lintFile(filePath) {
|
|
11743
|
+
const errors = [];
|
|
11744
|
+
const fileName = filePath.split("/").pop() || filePath;
|
|
11745
|
+
const fileNameError = this.validateFileName(fileName);
|
|
11746
|
+
if (fileNameError) {
|
|
11747
|
+
errors.push(fileNameError);
|
|
11748
|
+
}
|
|
11749
|
+
try {
|
|
11750
|
+
const content = await readFile2(filePath, "utf-8");
|
|
11751
|
+
this.errors = [];
|
|
11752
|
+
this.validateContent(fileName, content);
|
|
11753
|
+
errors.push(...this.errors);
|
|
11754
|
+
} catch (error2) {
|
|
11755
|
+
errors.push({
|
|
11756
|
+
file: fileName,
|
|
11757
|
+
message: `Failed to read file: ${error2}`,
|
|
11758
|
+
severity: "error"
|
|
11759
|
+
});
|
|
11760
|
+
}
|
|
11761
|
+
return errors;
|
|
11762
|
+
}
|
|
11763
|
+
/**
|
|
11764
|
+
* Lint all task files in the specified directory (FileSystem-specific)
|
|
11765
|
+
*/
|
|
11766
|
+
async lintDirectory(tasksDir) {
|
|
11767
|
+
this.errors = [];
|
|
11768
|
+
try {
|
|
11769
|
+
const files = await readdir(tasksDir);
|
|
11770
|
+
const taskFiles = files.filter((f) => f.endsWith(".md"));
|
|
11771
|
+
for (const file of taskFiles) {
|
|
11772
|
+
const fileNameError = this.validateFileName(file);
|
|
11773
|
+
if (fileNameError) {
|
|
11774
|
+
this.addError(file, fileNameError.message, fileNameError.severity);
|
|
11775
|
+
}
|
|
11776
|
+
const content = await readFile2(join4(tasksDir, file), "utf-8");
|
|
11777
|
+
this.validateContent(file, content);
|
|
11778
|
+
}
|
|
11779
|
+
const errors = this.errors.filter((e) => e.severity === "error");
|
|
11780
|
+
const warnings = this.errors.filter((e) => e.severity === "warning");
|
|
11781
|
+
return {
|
|
11782
|
+
errors,
|
|
11783
|
+
warnings,
|
|
11784
|
+
filesChecked: taskFiles.length,
|
|
11785
|
+
valid: errors.length === 0
|
|
11786
|
+
};
|
|
11787
|
+
} catch (error2) {
|
|
11788
|
+
throw new Error(`Failed to lint tasks: ${error2}`);
|
|
11789
|
+
}
|
|
11790
|
+
}
|
|
11791
|
+
static printResults(result) {
|
|
11792
|
+
if (result.errors.length === 0 && result.warnings.length === 0) {
|
|
11793
|
+
console.log(
|
|
11794
|
+
chalk6.green(`\u2705 All ${result.filesChecked} task files are valid!
|
|
11795
|
+
`)
|
|
11796
|
+
);
|
|
11797
|
+
return;
|
|
11798
|
+
}
|
|
11799
|
+
console.log(
|
|
11800
|
+
chalk6.bold(
|
|
11801
|
+
`
|
|
11802
|
+
\u{1F4CA} Validation Results (${result.filesChecked} files checked):
|
|
11803
|
+
`
|
|
11804
|
+
)
|
|
11805
|
+
);
|
|
11806
|
+
const errorsByFile = /* @__PURE__ */ new Map();
|
|
11807
|
+
[...result.errors, ...result.warnings].forEach((error2) => {
|
|
11808
|
+
if (!errorsByFile.has(error2.file)) {
|
|
11809
|
+
errorsByFile.set(error2.file, []);
|
|
11810
|
+
}
|
|
11811
|
+
errorsByFile.get(error2.file).push(error2);
|
|
11812
|
+
});
|
|
11813
|
+
for (const [file, fileErrors] of errorsByFile) {
|
|
11814
|
+
console.log(chalk6.cyan(`
|
|
11815
|
+
\u{1F4C4} ${file}`));
|
|
11816
|
+
for (const error2 of fileErrors) {
|
|
11817
|
+
const icon = error2.severity === "error" ? chalk6.red("\u274C") : chalk6.yellow("\u26A0\uFE0F");
|
|
11818
|
+
const location = error2.line ? `:${error2.line}` : "";
|
|
11819
|
+
console.log(` ${icon} ${error2.message}${location}`);
|
|
11820
|
+
}
|
|
11821
|
+
}
|
|
11822
|
+
console.log("\n" + "\u2500".repeat(60));
|
|
11823
|
+
console.log(
|
|
11824
|
+
chalk6.bold(
|
|
11825
|
+
`
|
|
11826
|
+
\u{1F4CA} Summary: ${chalk6.red(result.errors.length + " error(s)")}, ${chalk6.yellow(result.warnings.length + " warning(s)")}
|
|
11827
|
+
`
|
|
11828
|
+
)
|
|
11829
|
+
);
|
|
11830
|
+
}
|
|
11831
|
+
};
|
|
11832
|
+
|
|
11833
|
+
// src/lib/file-system-task-linter/file-system-task-linter.mock.ts
|
|
11834
|
+
init_esm_shims();
|
|
11835
|
+
function createMockFileValidationError(overrides) {
|
|
11836
|
+
return {
|
|
11837
|
+
file: "task-001.md",
|
|
11838
|
+
message: "Mock validation error",
|
|
11839
|
+
severity: "error",
|
|
11840
|
+
...overrides
|
|
11841
|
+
};
|
|
11842
|
+
}
|
|
11843
|
+
var MOCK_INVALID_LINT_RESULT = {
|
|
11844
|
+
errors: [
|
|
11845
|
+
createMockFileValidationError({
|
|
11846
|
+
file: "task-001.md",
|
|
11847
|
+
message: 'Invalid status: "invalid"'
|
|
11848
|
+
}),
|
|
11849
|
+
createMockFileValidationError({
|
|
11850
|
+
file: "task-002.md",
|
|
11851
|
+
message: 'Invalid type: "unknown"'
|
|
11852
|
+
})
|
|
11853
|
+
],
|
|
11854
|
+
filesChecked: 2,
|
|
11855
|
+
valid: false,
|
|
11856
|
+
warnings: []
|
|
11857
|
+
};
|
|
11858
|
+
var MOCK_WARNINGS_LINT_RESULT = {
|
|
11859
|
+
errors: [],
|
|
11860
|
+
filesChecked: 3,
|
|
11861
|
+
valid: true,
|
|
11862
|
+
warnings: [
|
|
11863
|
+
createMockFileValidationError({
|
|
11864
|
+
file: "task-003.md",
|
|
11865
|
+
message: "Consider adding assignee",
|
|
11866
|
+
severity: "warning"
|
|
11867
|
+
})
|
|
11868
|
+
]
|
|
11869
|
+
};
|
|
11870
|
+
var MOCK_VALIDATION_ERRORS = [
|
|
11871
|
+
createMockFileValidationError({
|
|
11872
|
+
file: "task-001.md",
|
|
11873
|
+
message: "Invalid status: must be one of [pending, in-progress, done, blocked]",
|
|
11874
|
+
line: 5
|
|
11875
|
+
}),
|
|
11876
|
+
createMockFileValidationError({
|
|
11877
|
+
file: "task-001.md",
|
|
11878
|
+
message: "Invalid type: must be one of [feat, fix, chore, docs, refactor, test]",
|
|
11879
|
+
line: 6
|
|
11880
|
+
})
|
|
11881
|
+
];
|
|
11882
|
+
|
|
11883
|
+
// src/lib/file-system-task-linter/file-system-task-linter.types.ts
|
|
11884
|
+
init_esm_shims();
|
|
11885
|
+
|
|
11821
11886
|
// src/taskin.ts
|
|
11822
11887
|
init_esm_shims();
|
|
11823
11888
|
var Taskin = class {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "taskin",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.12",
|
|
4
4
|
"description": "Task management system integrated with Git workflows",
|
|
5
5
|
"motivation": "Provide a CLI tool for task management integrated with Git workflows",
|
|
6
6
|
"solve": "Simplifies task tracking and management directly from the command line",
|
|
@@ -86,11 +86,11 @@
|
|
|
86
86
|
"typescript": "^5.9.3",
|
|
87
87
|
"vitest": "^1.6.1",
|
|
88
88
|
"@opentask/taskin-fs-provider": "1.0.5",
|
|
89
|
+
"@opentask/taskin-task-server-mcp": "0.1.0",
|
|
89
90
|
"@opentask/taskin-task-manager": "1.0.5",
|
|
91
|
+
"@opentask/taskin-types": "1.0.5",
|
|
90
92
|
"@opentask/taskin-task-server-ws": "0.1.0",
|
|
91
|
-
"@opentask/taskin-utils": "1.0.5"
|
|
92
|
-
"@opentask/taskin-task-server-mcp": "0.1.0",
|
|
93
|
-
"@opentask/taskin-types": "1.0.5"
|
|
93
|
+
"@opentask/taskin-utils": "1.0.5"
|
|
94
94
|
},
|
|
95
95
|
"scripts": {
|
|
96
96
|
"build": "pnpm run build:dashboard && tsup",
|