taskin 1.0.10 → 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/README.md +3 -3
- package/dist/index.js +559 -468
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -19,7 +19,7 @@ Taskin is a command-line tool that helps you manage tasks directly from your ter
|
|
|
19
19
|
|
|
20
20
|
## 🚀 Installation
|
|
21
21
|
|
|
22
|
-
|
|
22
|
+
```bash
|
|
23
23
|
|
|
24
24
|
# Using npx (recommended - no installation needed!)
|
|
25
25
|
|
|
@@ -36,9 +36,9 @@ pnpm add -g taskin
|
|
|
36
36
|
# Or with yarn
|
|
37
37
|
|
|
38
38
|
yarn global add taskin
|
|
39
|
-
|
|
39
|
+
```
|
|
40
40
|
|
|
41
|
-
> **Note:**
|
|
41
|
+
> **Note:** Please report any issues on [GitHub](https://github.com/sidartaveloso/taskin/issues).
|
|
42
42
|
|
|
43
43
|
## 📦 Available Packages
|
|
44
44
|
|
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,34 +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((line) =>
|
|
207
|
+
if (hasSectionStatus) {
|
|
208
|
+
const statusLineIdx = lines.findIndex((line) => sectionStatusPattern.test(line.trim()));
|
|
131
209
|
issues.push({
|
|
132
210
|
file: filePath,
|
|
133
211
|
line: statusLineIdx >= 0 ? statusLineIdx + 1 : void 0,
|
|
134
|
-
message: '
|
|
212
|
+
message: 'Section-based metadata ("## Status") is not allowed. Use inline format instead.',
|
|
135
213
|
severity: "error",
|
|
136
|
-
suggestion:
|
|
214
|
+
suggestion: `Replace section with inline metadata:
|
|
215
|
+
${i18n.status}: <todo|in-progress|done>`
|
|
137
216
|
});
|
|
138
217
|
}
|
|
139
|
-
if (!
|
|
218
|
+
if (!hasInlineStatus) {
|
|
140
219
|
issues.push({
|
|
141
220
|
file: filePath,
|
|
142
|
-
message: "Task file must have a
|
|
221
|
+
message: "Task file must have a Status field",
|
|
143
222
|
severity: "error",
|
|
144
|
-
suggestion:
|
|
223
|
+
suggestion: `Add inline metadata after title:
|
|
224
|
+
${i18n.status}: <todo|in-progress|done>`
|
|
145
225
|
});
|
|
146
226
|
} else {
|
|
147
|
-
const statusMatch = content.match(
|
|
148
|
-
const statusValue = statusMatch ? statusMatch[1]
|
|
149
|
-
if (![
|
|
150
|
-
|
|
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()));
|
|
151
238
|
issues.push({
|
|
152
239
|
file: filePath,
|
|
153
|
-
line: statusLineIdx >= 0 ? statusLineIdx +
|
|
154
|
-
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",
|
|
155
242
|
severity: "error",
|
|
156
|
-
suggestion: "Set status to: todo, in-progress, done, or
|
|
243
|
+
suggestion: "Set status to: todo, in-progress, done, pending, blocked, or canceled"
|
|
157
244
|
});
|
|
158
245
|
}
|
|
159
246
|
}
|
|
@@ -209,6 +296,7 @@ var init_task_validator = __esm({
|
|
|
209
296
|
"../fs-task-provider/dist/task-validator.js"() {
|
|
210
297
|
"use strict";
|
|
211
298
|
init_esm_shims();
|
|
299
|
+
init_i18n();
|
|
212
300
|
}
|
|
213
301
|
});
|
|
214
302
|
|
|
@@ -7053,47 +7141,10 @@ init_esm_shims();
|
|
|
7053
7141
|
|
|
7054
7142
|
// ../fs-task-provider/dist/fs-task-provider.js
|
|
7055
7143
|
init_esm_shims();
|
|
7144
|
+
init_i18n();
|
|
7145
|
+
init_task_validator();
|
|
7056
7146
|
import { promises as fs } from "fs";
|
|
7057
7147
|
import path2 from "path";
|
|
7058
|
-
|
|
7059
|
-
// ../fs-task-provider/dist/i18n.js
|
|
7060
|
-
init_esm_shims();
|
|
7061
|
-
var i18nConfig = {
|
|
7062
|
-
"en-US": {
|
|
7063
|
-
status: "Status",
|
|
7064
|
-
type: "Type",
|
|
7065
|
-
assignee: "Assignee",
|
|
7066
|
-
description: "Description",
|
|
7067
|
-
tasks: "Tasks",
|
|
7068
|
-
notes: "Notes",
|
|
7069
|
-
defaultAssignee: "To be defined",
|
|
7070
|
-
descriptionPlaceholder: "Add task description here...",
|
|
7071
|
-
notesPlaceholder: "Add any relevant notes or links here."
|
|
7072
|
-
},
|
|
7073
|
-
"pt-BR": {
|
|
7074
|
-
status: "Status",
|
|
7075
|
-
type: "Tipo",
|
|
7076
|
-
assignee: "Respons\xE1vel",
|
|
7077
|
-
description: "Descri\xE7\xE3o",
|
|
7078
|
-
tasks: "Tarefas",
|
|
7079
|
-
notes: "Notas",
|
|
7080
|
-
defaultAssignee: "A definir",
|
|
7081
|
-
descriptionPlaceholder: "Adicione a descri\xE7\xE3o da tarefa aqui...",
|
|
7082
|
-
notesPlaceholder: "Adicione notas ou links relevantes aqui."
|
|
7083
|
-
}
|
|
7084
|
-
};
|
|
7085
|
-
function getI18n(locale = "en-US") {
|
|
7086
|
-
return i18nConfig[locale];
|
|
7087
|
-
}
|
|
7088
|
-
function detectLocale(content) {
|
|
7089
|
-
if (content.includes("## Descri\xE7\xE3o") || content.includes("## Tipo") || content.includes("## Respons\xE1vel") || content.includes("## Tarefas")) {
|
|
7090
|
-
return "pt-BR";
|
|
7091
|
-
}
|
|
7092
|
-
return "en-US";
|
|
7093
|
-
}
|
|
7094
|
-
|
|
7095
|
-
// ../fs-task-provider/dist/fs-task-provider.js
|
|
7096
|
-
init_task_validator();
|
|
7097
7148
|
var FileSystemTaskProvider = class {
|
|
7098
7149
|
tasksDirectory;
|
|
7099
7150
|
userRegistry;
|
|
@@ -7115,19 +7166,20 @@ var FileSystemTaskProvider = class {
|
|
|
7115
7166
|
const title = titleMatch ? titleMatch[1].trim() : "Untitled";
|
|
7116
7167
|
const contentLocale = detectLocale(content);
|
|
7117
7168
|
const i18n = getI18n(contentLocale);
|
|
7118
|
-
const
|
|
7169
|
+
const extractInline = (name, localizedName) => {
|
|
7119
7170
|
const names = localizedName && localizedName !== name ? [localizedName, name] : [name];
|
|
7120
7171
|
for (const n of names) {
|
|
7121
|
-
const
|
|
7172
|
+
const escapedName = n.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
7173
|
+
const rx = new RegExp(`^${escapedName}:\\s*(.+)$`, "im");
|
|
7122
7174
|
const m = content.match(rx);
|
|
7123
7175
|
if (m)
|
|
7124
7176
|
return m[1].trim();
|
|
7125
7177
|
}
|
|
7126
7178
|
return null;
|
|
7127
7179
|
};
|
|
7128
|
-
const statusMatch =
|
|
7129
|
-
const typeMatch =
|
|
7130
|
-
const assigneeMatch =
|
|
7180
|
+
const statusMatch = extractInline("Status", i18n.status);
|
|
7181
|
+
const typeMatch = extractInline("Type", i18n.type);
|
|
7182
|
+
const assigneeMatch = extractInline("Assignee", i18n.assignee);
|
|
7131
7183
|
let assignee;
|
|
7132
7184
|
if (assigneeMatch) {
|
|
7133
7185
|
const assigneeValue = assigneeMatch.trim();
|
|
@@ -7151,19 +7203,17 @@ var FileSystemTaskProvider = class {
|
|
|
7151
7203
|
}
|
|
7152
7204
|
async updateTask(task) {
|
|
7153
7205
|
const currentContent = await fs.readFile(task.filePath, "utf-8");
|
|
7154
|
-
const
|
|
7155
|
-
if (
|
|
7206
|
+
const hasSectionMetadata = /##\s*(Status|Type|Assignee)/i.test(currentContent);
|
|
7207
|
+
if (hasSectionMetadata) {
|
|
7156
7208
|
const { fixTaskFile: fixTaskFile2 } = await Promise.resolve().then(() => (init_task_validator(), task_validator_exports));
|
|
7157
7209
|
await fixTaskFile2(task.filePath);
|
|
7158
7210
|
}
|
|
7159
7211
|
const content = await fs.readFile(task.filePath, "utf-8");
|
|
7160
7212
|
let updatedContent;
|
|
7161
|
-
if (
|
|
7162
|
-
updatedContent = content.replace(
|
|
7213
|
+
if (/^Status:\s*.+$/im.test(content)) {
|
|
7214
|
+
updatedContent = content.replace(/^Status:\s*.+$/im, `Status: ${task.status} `);
|
|
7163
7215
|
} else {
|
|
7164
|
-
updatedContent = content.replace(/(^#.*\n)/, `$
|
|
7165
|
-
## Status
|
|
7166
|
-
${task.status}
|
|
7216
|
+
updatedContent = content.replace(/(^#.*\n)/, `$1Status: ${task.status}
|
|
7167
7217
|
`);
|
|
7168
7218
|
}
|
|
7169
7219
|
await fs.writeFile(task.filePath, updatedContent, "utf-8");
|
|
@@ -7181,19 +7231,20 @@ ${task.status}
|
|
|
7181
7231
|
const title = titleMatch ? titleMatch[1].trim() : "Untitled";
|
|
7182
7232
|
const contentLocale = detectLocale(content);
|
|
7183
7233
|
const i18n = getI18n(contentLocale);
|
|
7184
|
-
const
|
|
7234
|
+
const extractInline = (name, localizedName) => {
|
|
7185
7235
|
const names = localizedName && localizedName !== name ? [localizedName, name] : [name];
|
|
7186
7236
|
for (const n of names) {
|
|
7187
|
-
const
|
|
7237
|
+
const escapedName = n.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
7238
|
+
const rx = new RegExp(`^${escapedName}:\\s*(.+)$`, "im");
|
|
7188
7239
|
const m = content.match(rx);
|
|
7189
7240
|
if (m)
|
|
7190
7241
|
return m[1].trim();
|
|
7191
7242
|
}
|
|
7192
7243
|
return null;
|
|
7193
7244
|
};
|
|
7194
|
-
const statusMatch =
|
|
7195
|
-
const typeMatch =
|
|
7196
|
-
const assigneeMatch =
|
|
7245
|
+
const statusMatch = extractInline("Status", i18n.status);
|
|
7246
|
+
const typeMatch = extractInline("Type", i18n.type);
|
|
7247
|
+
const assigneeMatch = extractInline("Assignee", i18n.assignee);
|
|
7197
7248
|
let assignee;
|
|
7198
7249
|
if (assigneeMatch) {
|
|
7199
7250
|
const assigneeValue = assigneeMatch.trim();
|
|
@@ -7217,8 +7268,13 @@ ${task.status}
|
|
|
7217
7268
|
return tasks;
|
|
7218
7269
|
}
|
|
7219
7270
|
async createTask(options) {
|
|
7220
|
-
const i18n = getI18n(this.locale);
|
|
7221
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);
|
|
7222
7278
|
const taskNumbers = allTasks.map((task2) => {
|
|
7223
7279
|
const match = task2.id.match(/^(\d+)$/);
|
|
7224
7280
|
return match ? parseInt(match[1], 10) : 0;
|
|
@@ -7262,30 +7318,19 @@ ${task.status}
|
|
|
7262
7318
|
const { id, title, type, description, assignee, i18n } = data;
|
|
7263
7319
|
return `# \u{1F9E9} Task ${id} \u2014 ${title}
|
|
7264
7320
|
|
|
7265
|
-
|
|
7266
|
-
|
|
7267
|
-
|
|
7268
|
-
|
|
7269
|
-
## ${i18n.type}
|
|
7270
|
-
|
|
7271
|
-
${type}
|
|
7272
|
-
|
|
7273
|
-
## ${i18n.assignee}
|
|
7274
|
-
|
|
7275
|
-
${assignee}
|
|
7321
|
+
${i18n.status}: pending
|
|
7322
|
+
${i18n.type}: ${type}
|
|
7323
|
+
${i18n.assignee}: ${assignee}
|
|
7276
7324
|
|
|
7277
7325
|
## ${i18n.description}
|
|
7278
|
-
|
|
7279
7326
|
${description || i18n.descriptionPlaceholder}
|
|
7280
7327
|
|
|
7281
7328
|
## ${i18n.tasks}
|
|
7282
|
-
|
|
7283
7329
|
- [ ] Task 1
|
|
7284
7330
|
- [ ] Task 2
|
|
7285
7331
|
- [ ] Task 3
|
|
7286
7332
|
|
|
7287
7333
|
## ${i18n.notes}
|
|
7288
|
-
|
|
7289
7334
|
${i18n.notesPlaceholder}
|
|
7290
7335
|
`;
|
|
7291
7336
|
}
|
|
@@ -7313,6 +7358,9 @@ ${i18n.notesPlaceholder}
|
|
|
7313
7358
|
}
|
|
7314
7359
|
};
|
|
7315
7360
|
|
|
7361
|
+
// ../fs-task-provider/dist/index.js
|
|
7362
|
+
init_i18n();
|
|
7363
|
+
|
|
7316
7364
|
// ../fs-task-provider/dist/user-registry.js
|
|
7317
7365
|
init_esm_shims();
|
|
7318
7366
|
import { promises as fs2 } from "fs";
|
|
@@ -7339,7 +7387,9 @@ var UserRegistry = class {
|
|
|
7339
7387
|
console.log(`[UserRegistry] Loaded ${this.users.size} users`);
|
|
7340
7388
|
} catch (error2) {
|
|
7341
7389
|
if (error2.code === "ENOENT") {
|
|
7342
|
-
console.warn(
|
|
7390
|
+
console.warn(
|
|
7391
|
+
"[UserRegistry] users.json not found, starting with empty registry"
|
|
7392
|
+
);
|
|
7343
7393
|
this.users.clear();
|
|
7344
7394
|
} else {
|
|
7345
7395
|
throw error2;
|
|
@@ -7358,12 +7408,10 @@ var UserRegistry = class {
|
|
|
7358
7408
|
*/
|
|
7359
7409
|
resolveUser(nameOrId) {
|
|
7360
7410
|
const byId = this.users.get(nameOrId);
|
|
7361
|
-
if (byId)
|
|
7362
|
-
return byId;
|
|
7411
|
+
if (byId) return byId;
|
|
7363
7412
|
const slug = nameOrId.toLowerCase().replace(/\s+/g, "-");
|
|
7364
7413
|
const bySlug = this.users.get(slug);
|
|
7365
|
-
if (bySlug)
|
|
7366
|
-
return bySlug;
|
|
7414
|
+
if (bySlug) return bySlug;
|
|
7367
7415
|
for (const user of this.users.values()) {
|
|
7368
7416
|
if (user.name.toLowerCase() === nameOrId.toLowerCase()) {
|
|
7369
7417
|
return user;
|
|
@@ -7403,7 +7451,11 @@ var UserRegistry = class {
|
|
|
7403
7451
|
const data = {
|
|
7404
7452
|
users: Object.fromEntries(this.users.entries())
|
|
7405
7453
|
};
|
|
7406
|
-
await fs2.writeFile(
|
|
7454
|
+
await fs2.writeFile(
|
|
7455
|
+
this.usersFilePath,
|
|
7456
|
+
JSON.stringify(data, null, 2),
|
|
7457
|
+
"utf-8"
|
|
7458
|
+
);
|
|
7407
7459
|
}
|
|
7408
7460
|
};
|
|
7409
7461
|
|
|
@@ -7808,38 +7860,41 @@ init_esm_shims();
|
|
|
7808
7860
|
// ../utils/dist/security.js
|
|
7809
7861
|
init_esm_shims();
|
|
7810
7862
|
import { z } from "zod";
|
|
7811
|
-
var HostSchema = z.string().refine(
|
|
7812
|
-
|
|
7813
|
-
return false;
|
|
7814
|
-
|
|
7815
|
-
|
|
7816
|
-
|
|
7817
|
-
|
|
7818
|
-
|
|
7819
|
-
|
|
7820
|
-
|
|
7821
|
-
|
|
7822
|
-
|
|
7823
|
-
|
|
7824
|
-
})
|
|
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."
|
|
7825
7881
|
}
|
|
7826
|
-
|
|
7827
|
-
return hostnameRegex.test(host);
|
|
7828
|
-
}, {
|
|
7829
|
-
message: "Invalid host. Must be localhost, a valid IPv4 address, or hostname."
|
|
7830
|
-
});
|
|
7882
|
+
);
|
|
7831
7883
|
var PortSchema = z.union([
|
|
7832
7884
|
z.number().int().min(1).max(65535),
|
|
7833
7885
|
z.string().regex(/^\d+$/).transform((val) => parseInt(val, 10)).pipe(z.number().int().min(1).max(65535))
|
|
7834
7886
|
]);
|
|
7835
|
-
var WebSocketUrlSchema = z.string().refine(
|
|
7836
|
-
|
|
7837
|
-
|
|
7838
|
-
|
|
7839
|
-
|
|
7840
|
-
|
|
7841
|
-
|
|
7842
|
-
}
|
|
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
|
+
);
|
|
7843
7898
|
var TaskIdSchema = z.string().min(1).max(100).regex(/^[a-zA-Z0-9_-]+$/, {
|
|
7844
7899
|
message: "Task ID must contain only alphanumeric characters, hyphens, and underscores."
|
|
7845
7900
|
});
|
|
@@ -7847,23 +7902,25 @@ var UserIdSchema = z.string().min(1).max(100).regex(/^[a-zA-Z0-9._-]+$/, {
|
|
|
7847
7902
|
message: "User ID must contain only alphanumeric characters, dots, hyphens, and underscores."
|
|
7848
7903
|
});
|
|
7849
7904
|
var EmailSchema = z.string().email().max(254);
|
|
7850
|
-
var SafePathSchema = z.string().refine(
|
|
7851
|
-
|
|
7852
|
-
return false;
|
|
7853
|
-
|
|
7854
|
-
|
|
7855
|
-
|
|
7856
|
-
|
|
7857
|
-
|
|
7858
|
-
|
|
7859
|
-
|
|
7860
|
-
|
|
7861
|
-
|
|
7862
|
-
|
|
7863
|
-
|
|
7864
|
-
},
|
|
7865
|
-
|
|
7866
|
-
|
|
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
|
+
);
|
|
7867
7924
|
var DashboardOptionsSchema = z.object({
|
|
7868
7925
|
host: HostSchema.optional(),
|
|
7869
7926
|
port: PortSchema.optional(),
|
|
@@ -8640,8 +8697,15 @@ async function setupFileSystemProvider(cwd) {
|
|
|
8640
8697
|
info(`${colors2.highlight("TASKS/")} directory already exists`);
|
|
8641
8698
|
success("\u2713 Directory is ready to use");
|
|
8642
8699
|
}
|
|
8643
|
-
const
|
|
8644
|
-
|
|
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");
|
|
8645
8709
|
info("Creating sample task...");
|
|
8646
8710
|
const sampleTask = `# Task 001 \u2014 Setup Project
|
|
8647
8711
|
|
|
@@ -8675,303 +8739,59 @@ You can edit or delete this file. Use \`taskin list\` to see all tasks.
|
|
|
8675
8739
|
|
|
8676
8740
|
// src/commands/lint.ts
|
|
8677
8741
|
init_esm_shims();
|
|
8678
|
-
import { join as join3 } from "path";
|
|
8679
|
-
|
|
8680
|
-
// src/lib/file-system-task-linter/index.ts
|
|
8681
|
-
init_esm_shims();
|
|
8682
|
-
|
|
8683
|
-
// src/lib/file-system-task-linter/file-system-task-linter.ts
|
|
8684
|
-
init_esm_shims();
|
|
8685
8742
|
import chalk4 from "chalk";
|
|
8686
|
-
import { readdir, readFile as readFile2 } from "fs/promises";
|
|
8687
8743
|
import { join as join2 } from "path";
|
|
8688
|
-
var
|
|
8689
|
-
|
|
8690
|
-
|
|
8691
|
-
|
|
8692
|
-
|
|
8693
|
-
|
|
8694
|
-
|
|
8695
|
-
|
|
8696
|
-
|
|
8697
|
-
|
|
8698
|
-
|
|
8699
|
-
|
|
8700
|
-
if (!pattern.test(fileName)) {
|
|
8701
|
-
return {
|
|
8702
|
-
file: fileName,
|
|
8703
|
-
message: "Invalid filename. Expected: task-NNN-kebab-case-title.md",
|
|
8704
|
-
severity: "error"
|
|
8705
|
-
};
|
|
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"
|
|
8706
8756
|
}
|
|
8707
|
-
|
|
8757
|
+
],
|
|
8758
|
+
handler: async (options) => {
|
|
8759
|
+
await executeLint(options);
|
|
8708
8760
|
}
|
|
8709
|
-
|
|
8710
|
-
|
|
8711
|
-
|
|
8712
|
-
|
|
8713
|
-
|
|
8714
|
-
|
|
8715
|
-
|
|
8716
|
-
|
|
8717
|
-
|
|
8718
|
-
};
|
|
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
|
+
`);
|
|
8719
8770
|
}
|
|
8720
|
-
|
|
8721
|
-
|
|
8722
|
-
|
|
8723
|
-
|
|
8724
|
-
|
|
8725
|
-
|
|
8726
|
-
|
|
8727
|
-
|
|
8728
|
-
|
|
8729
|
-
|
|
8730
|
-
|
|
8731
|
-
|
|
8732
|
-
|
|
8733
|
-
|
|
8734
|
-
|
|
8735
|
-
severity: "error"
|
|
8736
|
-
});
|
|
8737
|
-
}
|
|
8738
|
-
if (!metadata.type) {
|
|
8739
|
-
errors.push({
|
|
8740
|
-
file: filePath,
|
|
8741
|
-
message: "Missing required metadata: Type",
|
|
8742
|
-
severity: "error"
|
|
8743
|
-
});
|
|
8744
|
-
} else if (!VALID_TYPES.includes(metadata.type)) {
|
|
8745
|
-
errors.push({
|
|
8746
|
-
file: filePath,
|
|
8747
|
-
message: `Invalid type "${metadata.type}". Valid: ${VALID_TYPES.join(", ")}`,
|
|
8748
|
-
severity: "error"
|
|
8749
|
-
});
|
|
8750
|
-
}
|
|
8751
|
-
if (!metadata.assignee) {
|
|
8752
|
-
errors.push({
|
|
8753
|
-
file: filePath,
|
|
8754
|
-
message: "Missing recommended metadata: Assignee",
|
|
8755
|
-
severity: "warning"
|
|
8756
|
-
});
|
|
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}`));
|
|
8757
8786
|
}
|
|
8758
|
-
|
|
8787
|
+
console.log();
|
|
8759
8788
|
}
|
|
8760
|
-
|
|
8761
|
-
|
|
8762
|
-
|
|
8763
|
-
|
|
8764
|
-
|
|
8765
|
-
|
|
8766
|
-
"error",
|
|
8767
|
-
1
|
|
8768
|
-
);
|
|
8769
|
-
}
|
|
8770
|
-
const h1Pattern = /^#\s+(?:🧩\s+)?Task\s+\d{2,3}\s+[—-]\s+.+$/i;
|
|
8771
|
-
if (lines[0] && !h1Pattern.test(lines[0])) {
|
|
8772
|
-
this.addError(
|
|
8773
|
-
filename,
|
|
8774
|
-
'H1 heading must follow format: "# Task NNN \u2014 Title"',
|
|
8775
|
-
"error",
|
|
8776
|
-
1
|
|
8777
|
-
);
|
|
8778
|
-
}
|
|
8779
|
-
const metadata = this.extractMetadata(content);
|
|
8780
|
-
const metadataErrors = this.validateMetadata(metadata, filename);
|
|
8781
|
-
metadataErrors.forEach((error2) => {
|
|
8782
|
-
this.addError(error2.file, error2.message, error2.severity, error2.line);
|
|
8783
|
-
});
|
|
8784
|
-
const h2Sections = content.match(/^##\s+.+$/gm);
|
|
8785
|
-
if (!h2Sections || h2Sections.length === 0) {
|
|
8786
|
-
this.addError(
|
|
8787
|
-
filename,
|
|
8788
|
-
"Task should have at least one section (## heading)",
|
|
8789
|
-
"warning"
|
|
8790
|
-
);
|
|
8791
|
-
}
|
|
8792
|
-
const firstH2Index = content.indexOf("\n##");
|
|
8793
|
-
if (firstH2Index > -1) {
|
|
8794
|
-
const afterH2 = content.substring(firstH2Index);
|
|
8795
|
-
if (afterH2.match(/^Status:/im) || afterH2.match(/^Type:/im) || afterH2.match(/^Assignee:/im)) {
|
|
8796
|
-
this.addError(
|
|
8797
|
-
filename,
|
|
8798
|
-
"Metadata must be placed BEFORE the first ## section",
|
|
8799
|
-
"error"
|
|
8800
|
-
);
|
|
8801
|
-
}
|
|
8802
|
-
}
|
|
8803
|
-
}
|
|
8804
|
-
/**
|
|
8805
|
-
* Lint a single task file (FileSystem-specific)
|
|
8806
|
-
*/
|
|
8807
|
-
async lintFile(filePath) {
|
|
8808
|
-
const errors = [];
|
|
8809
|
-
const fileName = filePath.split("/").pop() || filePath;
|
|
8810
|
-
const fileNameError = this.validateFileName(fileName);
|
|
8811
|
-
if (fileNameError) {
|
|
8812
|
-
errors.push(fileNameError);
|
|
8813
|
-
}
|
|
8814
|
-
try {
|
|
8815
|
-
const content = await readFile2(filePath, "utf-8");
|
|
8816
|
-
this.errors = [];
|
|
8817
|
-
this.validateContent(fileName, content);
|
|
8818
|
-
errors.push(...this.errors);
|
|
8819
|
-
} catch (error2) {
|
|
8820
|
-
errors.push({
|
|
8821
|
-
file: fileName,
|
|
8822
|
-
message: `Failed to read file: ${error2}`,
|
|
8823
|
-
severity: "error"
|
|
8824
|
-
});
|
|
8825
|
-
}
|
|
8826
|
-
return errors;
|
|
8827
|
-
}
|
|
8828
|
-
/**
|
|
8829
|
-
* Lint all task files in the specified directory (FileSystem-specific)
|
|
8830
|
-
*/
|
|
8831
|
-
async lintDirectory(tasksDir) {
|
|
8832
|
-
this.errors = [];
|
|
8833
|
-
try {
|
|
8834
|
-
const files = await readdir(tasksDir);
|
|
8835
|
-
const taskFiles = files.filter((f) => f.endsWith(".md"));
|
|
8836
|
-
for (const file of taskFiles) {
|
|
8837
|
-
const fileNameError = this.validateFileName(file);
|
|
8838
|
-
if (fileNameError) {
|
|
8839
|
-
this.addError(file, fileNameError.message, fileNameError.severity);
|
|
8840
|
-
}
|
|
8841
|
-
const content = await readFile2(join2(tasksDir, file), "utf-8");
|
|
8842
|
-
this.validateContent(file, content);
|
|
8843
|
-
}
|
|
8844
|
-
const errors = this.errors.filter((e) => e.severity === "error");
|
|
8845
|
-
const warnings = this.errors.filter((e) => e.severity === "warning");
|
|
8846
|
-
return {
|
|
8847
|
-
errors,
|
|
8848
|
-
warnings,
|
|
8849
|
-
filesChecked: taskFiles.length,
|
|
8850
|
-
valid: errors.length === 0
|
|
8851
|
-
};
|
|
8852
|
-
} catch (error2) {
|
|
8853
|
-
throw new Error(`Failed to lint tasks: ${error2}`);
|
|
8854
|
-
}
|
|
8855
|
-
}
|
|
8856
|
-
static printResults(result) {
|
|
8857
|
-
if (result.errors.length === 0 && result.warnings.length === 0) {
|
|
8858
|
-
console.log(
|
|
8859
|
-
chalk4.green(`\u2705 All ${result.filesChecked} task files are valid!
|
|
8860
|
-
`)
|
|
8861
|
-
);
|
|
8862
|
-
return;
|
|
8863
|
-
}
|
|
8864
|
-
console.log(
|
|
8865
|
-
chalk4.bold(
|
|
8866
|
-
`
|
|
8867
|
-
\u{1F4CA} Validation Results (${result.filesChecked} files checked):
|
|
8868
|
-
`
|
|
8869
|
-
)
|
|
8870
|
-
);
|
|
8871
|
-
const errorsByFile = /* @__PURE__ */ new Map();
|
|
8872
|
-
[...result.errors, ...result.warnings].forEach((error2) => {
|
|
8873
|
-
if (!errorsByFile.has(error2.file)) {
|
|
8874
|
-
errorsByFile.set(error2.file, []);
|
|
8875
|
-
}
|
|
8876
|
-
errorsByFile.get(error2.file).push(error2);
|
|
8877
|
-
});
|
|
8878
|
-
for (const [file, fileErrors] of errorsByFile) {
|
|
8879
|
-
console.log(chalk4.cyan(`
|
|
8880
|
-
\u{1F4C4} ${file}`));
|
|
8881
|
-
for (const error2 of fileErrors) {
|
|
8882
|
-
const icon = error2.severity === "error" ? chalk4.red("\u274C") : chalk4.yellow("\u26A0\uFE0F");
|
|
8883
|
-
const location = error2.line ? `:${error2.line}` : "";
|
|
8884
|
-
console.log(` ${icon} ${error2.message}${location}`);
|
|
8885
|
-
}
|
|
8886
|
-
}
|
|
8887
|
-
console.log("\n" + "\u2500".repeat(60));
|
|
8888
|
-
console.log(
|
|
8889
|
-
chalk4.bold(
|
|
8890
|
-
`
|
|
8891
|
-
\u{1F4CA} Summary: ${chalk4.red(result.errors.length + " error(s)")}, ${chalk4.yellow(result.warnings.length + " warning(s)")}
|
|
8892
|
-
`
|
|
8893
|
-
)
|
|
8894
|
-
);
|
|
8895
|
-
}
|
|
8896
|
-
};
|
|
8897
|
-
|
|
8898
|
-
// src/lib/file-system-task-linter/file-system-task-linter.mock.ts
|
|
8899
|
-
init_esm_shims();
|
|
8900
|
-
function createMockFileValidationError(overrides) {
|
|
8901
|
-
return {
|
|
8902
|
-
file: "task-001.md",
|
|
8903
|
-
message: "Mock validation error",
|
|
8904
|
-
severity: "error",
|
|
8905
|
-
...overrides
|
|
8906
|
-
};
|
|
8907
|
-
}
|
|
8908
|
-
var MOCK_INVALID_LINT_RESULT = {
|
|
8909
|
-
errors: [
|
|
8910
|
-
createMockFileValidationError({
|
|
8911
|
-
file: "task-001.md",
|
|
8912
|
-
message: 'Invalid status: "invalid"'
|
|
8913
|
-
}),
|
|
8914
|
-
createMockFileValidationError({
|
|
8915
|
-
file: "task-002.md",
|
|
8916
|
-
message: 'Invalid type: "unknown"'
|
|
8917
|
-
})
|
|
8918
|
-
],
|
|
8919
|
-
filesChecked: 2,
|
|
8920
|
-
valid: false,
|
|
8921
|
-
warnings: []
|
|
8922
|
-
};
|
|
8923
|
-
var MOCK_WARNINGS_LINT_RESULT = {
|
|
8924
|
-
errors: [],
|
|
8925
|
-
filesChecked: 3,
|
|
8926
|
-
valid: true,
|
|
8927
|
-
warnings: [
|
|
8928
|
-
createMockFileValidationError({
|
|
8929
|
-
file: "task-003.md",
|
|
8930
|
-
message: "Consider adding assignee",
|
|
8931
|
-
severity: "warning"
|
|
8932
|
-
})
|
|
8933
|
-
]
|
|
8934
|
-
};
|
|
8935
|
-
var MOCK_VALIDATION_ERRORS = [
|
|
8936
|
-
createMockFileValidationError({
|
|
8937
|
-
file: "task-001.md",
|
|
8938
|
-
message: "Invalid status: must be one of [pending, in-progress, done, blocked]",
|
|
8939
|
-
line: 5
|
|
8940
|
-
}),
|
|
8941
|
-
createMockFileValidationError({
|
|
8942
|
-
file: "task-001.md",
|
|
8943
|
-
message: "Invalid type: must be one of [feat, fix, chore, docs, refactor, test]",
|
|
8944
|
-
line: 6
|
|
8945
|
-
})
|
|
8946
|
-
];
|
|
8947
|
-
|
|
8948
|
-
// src/lib/file-system-task-linter/file-system-task-linter.types.ts
|
|
8949
|
-
init_esm_shims();
|
|
8950
|
-
|
|
8951
|
-
// src/commands/lint.ts
|
|
8952
|
-
var lintCommand = defineCommand({
|
|
8953
|
-
name: "lint",
|
|
8954
|
-
description: "\u{1F50D} Validate task markdown files",
|
|
8955
|
-
options: [
|
|
8956
|
-
{
|
|
8957
|
-
flags: "-p, --path <directory>",
|
|
8958
|
-
description: "Path to TASKS directory",
|
|
8959
|
-
defaultValue: "TASKS"
|
|
8960
|
-
}
|
|
8961
|
-
],
|
|
8962
|
-
handler: async (options) => {
|
|
8963
|
-
await executeLint(options);
|
|
8964
|
-
}
|
|
8965
|
-
});
|
|
8966
|
-
async function executeLint(options) {
|
|
8967
|
-
const tasksDir = options.path || join3(process.cwd(), "TASKS");
|
|
8968
|
-
console.log(`\u{1F4CB} Linting task files in: ${tasksDir}
|
|
8969
|
-
`);
|
|
8970
|
-
const linter = new FileSystemTaskLinter();
|
|
8971
|
-
const result = await linter.lintDirectory(tasksDir);
|
|
8972
|
-
FileSystemTaskLinter.printResults(result);
|
|
8973
|
-
if (!result.valid) {
|
|
8974
|
-
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);
|
|
8975
8795
|
}
|
|
8976
8796
|
}
|
|
8977
8797
|
|
|
@@ -11774,13 +11594,13 @@ function showCustomHelp() {
|
|
|
11774
11594
|
// src/version.ts
|
|
11775
11595
|
init_esm_shims();
|
|
11776
11596
|
import { readFileSync } from "fs";
|
|
11777
|
-
import { dirname, join as
|
|
11597
|
+
import { dirname, join as join3 } from "path";
|
|
11778
11598
|
import { fileURLToPath as fileURLToPath3 } from "url";
|
|
11779
11599
|
var __filename3 = fileURLToPath3(import.meta.url);
|
|
11780
11600
|
var __dirname3 = dirname(__filename3);
|
|
11781
11601
|
function getVersion() {
|
|
11782
11602
|
try {
|
|
11783
|
-
const packageJsonPath =
|
|
11603
|
+
const packageJsonPath = join3(__dirname3, "../package.json");
|
|
11784
11604
|
const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf-8"));
|
|
11785
11605
|
return packageJson.version;
|
|
11786
11606
|
} catch {
|
|
@@ -11792,6 +11612,277 @@ function getVersion() {
|
|
|
11792
11612
|
init_esm_shims();
|
|
11793
11613
|
import { dirname as dirname2, join as join5 } from "path";
|
|
11794
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
|
+
|
|
11795
11886
|
// src/taskin.ts
|
|
11796
11887
|
init_esm_shims();
|
|
11797
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,10 +86,10 @@
|
|
|
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-manager": "1.0.5",
|
|
90
|
-
"@opentask/taskin-task-server-ws": "0.1.0",
|
|
91
89
|
"@opentask/taskin-task-server-mcp": "0.1.0",
|
|
90
|
+
"@opentask/taskin-task-manager": "1.0.5",
|
|
92
91
|
"@opentask/taskin-types": "1.0.5",
|
|
92
|
+
"@opentask/taskin-task-server-ws": "0.1.0",
|
|
93
93
|
"@opentask/taskin-utils": "1.0.5"
|
|
94
94
|
},
|
|
95
95
|
"scripts": {
|