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.
Files changed (3) hide show
  1. package/README.md +3 -3
  2. package/dist/index.js +559 -468
  3. 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
- \`\`\`bash
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:** This is a beta version. Please report any issues on [GitHub](https://github.com/sidartaveloso/taskin/issues).
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 hasInlineStatus = /^Status:\s*.+$/im.test(content);
66
- const hasInlineType = /^Type:\s*.+$/im.test(content);
67
- const hasInlineAssignee = /^Assignee:\s*.+$/im.test(content);
68
- if (!hasInlineStatus && !hasInlineType && !hasInlineAssignee) {
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
- if (statusMatch) {
76
- newContent = newContent.replace(/^Status:\s*.+$/im, "");
77
- }
78
- if (typeMatch) {
79
- newContent = newContent.replace(/^Type:\s*.+$/im, "");
80
- }
81
- if (assigneeMatch) {
82
- newContent = newContent.replace(/^Assignee:\s*.+$/im, "");
83
- }
84
- newContent = newContent.replace(/\n{3,}/g, "\n\n");
85
- const titleLineIdx = newContent.split("\n").findIndex((line) => line.trim().startsWith("# "));
86
- if (titleLineIdx === -1) {
87
- return false;
88
- }
89
- const contentLines = newContent.split("\n");
90
- const beforeTitle = contentLines.slice(0, titleLineIdx + 1);
91
- const afterTitle = contentLines.slice(titleLineIdx + 1);
92
- const sections = [];
93
- if (statusMatch) {
94
- sections.push("", "## Status", "", statusMatch[1].trim());
95
- }
96
- if (typeMatch) {
97
- sections.push("", "## Type", "", typeMatch[1].trim());
98
- }
99
- if (assigneeMatch) {
100
- sections.push("", "## Assignee", "", assigneeMatch[1].trim());
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
- const fixed = [...beforeTitle, ...sections, "", ...afterTitle].join("\n");
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 hasStatusSection = content.includes("## Status");
118
- const hasInlineStatus = /^Status:\s*.+$/im.test(content);
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 (hasInlineStatus) {
130
- const statusLineIdx = lines.findIndex((line) => /^Status:/i.test(line.trim()));
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: 'Inline metadata ("Status: ...") is not allowed. Use a "## Status" section instead.',
212
+ message: 'Section-based metadata ("## Status") is not allowed. Use inline format instead.',
135
213
  severity: "error",
136
- suggestion: "Replace inline metadata with a section:\n## Status\n<todo|in-progress|done>"
214
+ suggestion: `Replace section with inline metadata:
215
+ ${i18n.status}: <todo|in-progress|done>`
137
216
  });
138
217
  }
139
- if (!hasStatusSection) {
218
+ if (!hasInlineStatus) {
140
219
  issues.push({
141
220
  file: filePath,
142
- message: "Task file must have a ## Status section",
221
+ message: "Task file must have a Status field",
143
222
  severity: "error",
144
- suggestion: "Add a section:\n## Status\n<todo|in-progress|done>"
223
+ suggestion: `Add inline metadata after title:
224
+ ${i18n.status}: <todo|in-progress|done>`
145
225
  });
146
226
  } else {
147
- const statusMatch = content.match(/## Status\s*\n\s*([^\n\r]+)/i);
148
- const statusValue = statusMatch ? statusMatch[1].trim().toLowerCase() : "";
149
- if (!["todo", "in-progress", "done", "pending"].includes(statusValue)) {
150
- const statusLineIdx = lines.findIndex((line) => line.trim() === "## Status");
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 + 2 : void 0,
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 pending"
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 extractSection = (name, localizedName) => {
7169
+ const extractInline = (name, localizedName) => {
7119
7170
  const names = localizedName && localizedName !== name ? [localizedName, name] : [name];
7120
7171
  for (const n of names) {
7121
- const rx = new RegExp(`##\\s*${n}\\s*\\n\\s*([^\\n\\r]+)`, "i");
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 = extractSection("Status", i18n.status);
7129
- const typeMatch = extractSection("Type", i18n.type);
7130
- const assigneeMatch = extractSection("Assignee", i18n.assignee);
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 hasInlineMetadata = /^(Status|Type|Assignee):\s*.+$/im.test(currentContent);
7155
- if (hasInlineMetadata) {
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 (/##\s*Status/i.test(content)) {
7162
- updatedContent = content.replace(/(##\s*Status\s*\n\s*)([^\n\r]*)/i, `$1${task.status}`);
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)/, `$1
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 extractSection = (name, localizedName) => {
7234
+ const extractInline = (name, localizedName) => {
7185
7235
  const names = localizedName && localizedName !== name ? [localizedName, name] : [name];
7186
7236
  for (const n of names) {
7187
- const rx = new RegExp(`##\\s*${n}\\s*\\n\\s*([^\\n\\r]+)`, "i");
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 = extractSection("Status", i18n.status);
7195
- const typeMatch = extractSection("Type", i18n.type);
7196
- const assigneeMatch = extractSection("Assignee", i18n.assignee);
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
- ## ${i18n.status}
7266
-
7267
- pending
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("[UserRegistry] users.json not found, starting with empty registry");
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(this.usersFilePath, JSON.stringify(data, null, 2), "utf-8");
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((host) => {
7812
- if (!host || host.length === 0)
7813
- return false;
7814
- if (host === "localhost")
7815
- return true;
7816
- const parts = host.split(".");
7817
- const allNumeric = parts.every((p) => /^\d+$/.test(p));
7818
- if (allNumeric) {
7819
- if (parts.length !== 4)
7820
- return false;
7821
- return parts.every((part) => {
7822
- const num = parseInt(part, 10);
7823
- return !isNaN(num) && num >= 0 && num <= 255 && part === num.toString();
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
- 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])?)*$/;
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((url) => {
7836
- try {
7837
- const parsed = new URL(url);
7838
- return parsed.protocol === "ws:" || parsed.protocol === "wss:";
7839
- } catch {
7840
- return false;
7841
- }
7842
- }, { message: "Invalid WebSocket URL. Must use ws:// or wss:// protocol." });
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((filePath) => {
7851
- if (!filePath || filePath.length === 0)
7852
- return false;
7853
- const dangerousPatterns = [
7854
- /\.\./,
7855
- // Parent directory (..)
7856
- /~\//,
7857
- // Home directory
7858
- /^\//,
7859
- // Absolute path
7860
- /^[A-Za-z]:\\/
7861
- // Windows absolute path
7862
- ];
7863
- return !dangerousPatterns.some((pattern) => pattern.test(filePath));
7864
- }, {
7865
- message: "Invalid path. Must be a relative path without traversal patterns."
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 sampleTaskFile = join(tasksDir, "task-001-setup-project.md");
8644
- if (!existsSync4(sampleTaskFile)) {
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 VALID_STATUSES = ["pending", "in-progress", "done", "blocked"];
8689
- var VALID_TYPES = ["feat", "fix", "chore", "docs", "refactor", "test"];
8690
- var FileSystemTaskLinter = class {
8691
- errors = [];
8692
- addError(file, message, severity = "error", line) {
8693
- this.errors.push({ file, message, severity, line });
8694
- }
8695
- /**
8696
- * Validate task file name format (FileSystem-specific)
8697
- */
8698
- validateFileName(fileName) {
8699
- const pattern = /^task-\d{2,3}-[a-z0-9-]+\.md$/;
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
- return null;
8757
+ ],
8758
+ handler: async (options) => {
8759
+ await executeLint(options);
8708
8760
  }
8709
- extractMetadata(content) {
8710
- const headerSection = content.split(/^##/m)[0];
8711
- const statusMatch = headerSection.match(/^Status:\s*(.+)$/im);
8712
- const typeMatch = headerSection.match(/^Type:\s*(.+)$/im);
8713
- const assigneeMatch = headerSection.match(/^Assignee:\s*(.+)$/im);
8714
- return {
8715
- status: statusMatch?.[1]?.trim().toLowerCase(),
8716
- type: typeMatch?.[1]?.trim().toLowerCase(),
8717
- assignee: assigneeMatch?.[1]?.trim()
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
- * Validate task metadata (FileSystem-specific)
8722
- */
8723
- validateMetadata(metadata, filePath) {
8724
- const errors = [];
8725
- if (!metadata.status) {
8726
- errors.push({
8727
- file: filePath,
8728
- message: "Missing required metadata: Status",
8729
- severity: "error"
8730
- });
8731
- } else if (!VALID_STATUSES.includes(metadata.status)) {
8732
- errors.push({
8733
- file: filePath,
8734
- message: `Invalid status "${metadata.status}". Valid: ${VALID_STATUSES.join(", ")}`,
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
- return errors;
8787
+ console.log();
8759
8788
  }
8760
- validateContent(filename, content) {
8761
- const lines = content.split("\n");
8762
- if (!lines[0]?.trim().startsWith("# ")) {
8763
- this.addError(
8764
- filename,
8765
- "Task file must start with a level 1 heading (# Task NNN \u2014 Title)",
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 join4 } from "path";
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 = join4(__dirname3, "../package.json");
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.10",
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": {