imdone-core 2.0.0 → 2.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (160) hide show
  1. package/.dependency-cruiser.cjs +412 -0
  2. package/.github/ISSUE_TEMPLATE/bug_report.md +37 -0
  3. package/.github/workflows/ci.yml +24 -0
  4. package/.imdoneignore +13 -0
  5. package/.mocharc.json +10 -0
  6. package/.prettierrc +6 -0
  7. package/CHANGELOG.md +160 -0
  8. package/_index.md +7 -0
  9. package/_navbar.md +1 -0
  10. package/build.js +57 -0
  11. package/esm-migration.md +446 -0
  12. package/index.html +184 -0
  13. package/lib/FileProjectContext.js +13 -0
  14. package/lib/ProjectContext.js +43 -0
  15. package/lib/adapters/date-time.js +15 -0
  16. package/lib/adapters/environment.js +3 -0
  17. package/lib/adapters/file-gateway.js +154 -0
  18. package/lib/adapters/git-download.js +55 -0
  19. package/lib/adapters/index.js +9 -0
  20. package/lib/adapters/logger.js +136 -0
  21. package/lib/adapters/markdown.js +180 -0
  22. package/lib/adapters/parsers/DateLanguageParser.js +12 -0
  23. package/lib/adapters/parsers/content-transformer.js +116 -0
  24. package/lib/adapters/parsers/file/LocalFileParserFactory.js +36 -0
  25. package/lib/adapters/parsers/file/ReadStreamCodeParser.js +126 -0
  26. package/lib/adapters/parsers/file/ReadStreamMarkdownParser.js +17 -0
  27. package/lib/adapters/parsers/file/ReadStreamTaskParser.js +106 -0
  28. package/lib/adapters/parsers/file/index.js +4 -0
  29. package/lib/adapters/parsers/function-parser.js +25 -0
  30. package/lib/adapters/parsers/index.js +5 -0
  31. package/lib/adapters/parsers/task/CardContentParser.js +367 -0
  32. package/lib/adapters/parsers/task/CheckStyleTaskParser.js +28 -0
  33. package/lib/adapters/parsers/task/CodeStyleTaskParser.js +24 -0
  34. package/lib/adapters/parsers/task/HashStyleTaskParser.js +27 -0
  35. package/lib/adapters/parsers/task/MarkdownStyleTaskParser.js +24 -0
  36. package/lib/adapters/parsers/task/TaskParser.js +38 -0
  37. package/lib/adapters/parsers/task/index.js +6 -0
  38. package/lib/adapters/storage/config.js +44 -0
  39. package/lib/adapters/storage/index.js +2 -0
  40. package/lib/adapters/storage/tags.js +29 -0
  41. package/lib/adapters/yaml.js +9 -0
  42. package/lib/card.js +299 -0
  43. package/lib/checksum.js +22 -0
  44. package/lib/config.js +249 -0
  45. package/lib/constants.js +68 -0
  46. package/lib/context/ApplicationContext.js +35 -0
  47. package/lib/context/index.js +1 -0
  48. package/lib/default-settings.js +37 -0
  49. package/lib/domain/entities/View.js +23 -0
  50. package/lib/domain/entities/index.js +1 -0
  51. package/lib/domain/index.js +2 -0
  52. package/lib/domain/usecases/GetViewsUsecase.js +44 -0
  53. package/lib/domain/usecases/SaveViewUsecase.js +51 -0
  54. package/lib/domain/usecases/index.js +2 -0
  55. package/lib/file.js +1391 -0
  56. package/lib/get-tasks-to-modify.js +117 -0
  57. package/lib/languages.js +113 -0
  58. package/lib/list.js +56 -0
  59. package/lib/migrate-config.js +165 -0
  60. package/lib/mixins/index.js +2 -0
  61. package/lib/mixins/repo-fs-store.js +359 -0
  62. package/lib/mixins/repo-watched-fs-store.js +206 -0
  63. package/lib/plugins/archive-plugin.js +72 -0
  64. package/lib/plugins/default-board-actions-plugin.js +56 -0
  65. package/lib/plugins/default-board-properties-plugin.js +101 -0
  66. package/lib/plugins/default-plugins.js +7 -0
  67. package/lib/plugins/epic-plugin.js +177 -0
  68. package/lib/plugins/extension-plugin.js +94 -0
  69. package/lib/plugins/index.js +9 -0
  70. package/lib/plugins/persist-tags-plugin.js +14 -0
  71. package/lib/plugins/plugin-manager.js +458 -0
  72. package/lib/plugins/plugin-registry.js +57 -0
  73. package/lib/project-factory.js +42 -0
  74. package/lib/project.js +864 -0
  75. package/lib/repository.js +1514 -0
  76. package/lib/task.js +780 -0
  77. package/lib/tools.js +176 -0
  78. package/lib/usecases/actions.js +32 -0
  79. package/lib/usecases/get-imdone-url.js +6 -0
  80. package/lib/usecases/get-project-tags.js +7 -0
  81. package/lib/usecases/get-tasks-in-file.js +26 -0
  82. package/lib/usecases/index.js +4 -0
  83. package/package.json +2 -6
  84. package/scripts/delete-markdown-indexes.js +87 -0
  85. package/scripts/generate-index.js +35 -0
  86. package/scripts/index-markdown.js +55 -0
  87. package/tsconfig.json +15 -0
  88. package/types/FileProjectContext.d.ts +4 -0
  89. package/types/ProjectContext.d.ts +9 -0
  90. package/types/adapters/date-time.d.ts +2 -0
  91. package/types/adapters/environment.d.ts +1 -0
  92. package/types/adapters/file-gateway.d.ts +51 -0
  93. package/types/adapters/git-download.d.ts +1 -0
  94. package/types/adapters/index.d.ts +9 -0
  95. package/types/adapters/logger.d.ts +24 -0
  96. package/types/adapters/markdown.d.ts +13 -0
  97. package/types/adapters/parsers/DateLanguageParser.d.ts +1 -0
  98. package/types/adapters/parsers/content-transformer.d.ts +10 -0
  99. package/types/adapters/parsers/file/LocalFileParserFactory.d.ts +5 -0
  100. package/types/adapters/parsers/file/ReadStreamCodeParser.d.ts +13 -0
  101. package/types/adapters/parsers/file/ReadStreamMarkdownParser.d.ts +7 -0
  102. package/types/adapters/parsers/file/ReadStreamTaskParser.d.ts +19 -0
  103. package/types/adapters/parsers/file/index.d.ts +4 -0
  104. package/types/adapters/parsers/function-parser.d.ts +1 -0
  105. package/types/adapters/parsers/index.d.ts +5 -0
  106. package/types/adapters/parsers/task/CardContentParser.d.ts +45 -0
  107. package/types/adapters/parsers/task/CheckStyleTaskParser.d.ts +12 -0
  108. package/types/adapters/parsers/task/CodeStyleTaskParser.d.ts +14 -0
  109. package/types/adapters/parsers/task/HashStyleTaskParser.d.ts +13 -0
  110. package/types/adapters/parsers/task/MarkdownStyleTaskParser.d.ts +14 -0
  111. package/types/adapters/parsers/task/TaskParser.d.ts +7 -0
  112. package/types/adapters/parsers/task/index.d.ts +6 -0
  113. package/types/adapters/storage/config.d.ts +6 -0
  114. package/types/adapters/storage/index.d.ts +2 -0
  115. package/types/adapters/storage/tags.d.ts +3 -0
  116. package/types/adapters/yaml.d.ts +2 -0
  117. package/types/card.d.ts +166 -0
  118. package/types/checksum.d.ts +8 -0
  119. package/types/config.d.ts +61 -0
  120. package/types/constants.d.ts +46 -0
  121. package/types/context/ApplicationContext.d.ts +24 -0
  122. package/types/context/index.d.ts +1 -0
  123. package/types/default-settings.d.ts +2 -0
  124. package/types/domain/entities/FileProjectContext.d.ts +5 -0
  125. package/types/domain/entities/View.d.ts +17 -0
  126. package/types/domain/entities/index.d.ts +1 -0
  127. package/types/domain/index.d.ts +2 -0
  128. package/types/domain/usecases/GetViewsUsecase.d.ts +4 -0
  129. package/types/domain/usecases/SaveViewUsecase.d.ts +8 -0
  130. package/types/domain/usecases/index.d.ts +2 -0
  131. package/types/file.d.ts +344 -0
  132. package/types/get-tasks-to-modify.d.ts +3 -0
  133. package/types/index.d.ts +23 -0
  134. package/types/languages.d.ts +649 -0
  135. package/types/list.d.ts +30 -0
  136. package/types/migrate-config.d.ts +1 -0
  137. package/types/mixins/index.d.ts +2 -0
  138. package/types/mixins/repo-fs-store.d.ts +2 -0
  139. package/types/mixins/repo-watched-fs-store.d.ts +2 -0
  140. package/types/plugins/archive-plugin.d.ts +8 -0
  141. package/types/plugins/default-board-actions-plugin.d.ts +9 -0
  142. package/types/plugins/default-board-properties-plugin.d.ts +16 -0
  143. package/types/plugins/default-plugins.d.ts +6 -0
  144. package/types/plugins/epic-plugin.d.ts +19 -0
  145. package/types/plugins/extension-plugin.d.ts +15 -0
  146. package/types/plugins/index.d.ts +9 -0
  147. package/types/plugins/persist-tags-plugin.d.ts +4 -0
  148. package/types/plugins/plugin-manager.d.ts +68 -0
  149. package/types/plugins/plugin-registry.d.ts +1 -0
  150. package/types/project-factory.d.ts +14 -0
  151. package/types/project.d.ts +158 -0
  152. package/types/repository.d.ts +996 -0
  153. package/types/task.d.ts +202 -0
  154. package/types/tools.d.ts +29 -0
  155. package/types/usecases/actions.d.ts +7 -0
  156. package/types/usecases/get-imdone-url.d.ts +1 -0
  157. package/types/usecases/get-project-tags.d.ts +1 -0
  158. package/types/usecases/get-tasks-in-file.d.ts +4 -0
  159. package/types/usecases/index.d.ts +4 -0
  160. package/vitest.config.js +13 -0
@@ -0,0 +1,359 @@
1
+ import _path from 'path'
2
+ import _cloneDeep from 'lodash.clonedeep'
3
+ import _isFunction from 'lodash.isfunction'
4
+ import _isUndefined from 'lodash.isundefined'
5
+ import _assign from 'lodash.assign'
6
+ import _remove from 'lodash.remove'
7
+ import _union from 'lodash.union'
8
+ import { eachLimit } from 'async'
9
+ import { computeChecksum } from '../checksum.js'
10
+ import { constants } from '../constants.js'
11
+ import debug from 'debug'
12
+ const log = debug('imdone-mixins:repo-fs-store.js')
13
+ import { File } from '../file.js'
14
+ import { Config } from '../config.js'
15
+ import { languages } from '../languages.js'
16
+ import { isBinaryFile } from 'isbinaryfile'
17
+ import eol from 'eol'
18
+ const _eol = String(eol.auto)
19
+ import readdirp from 'readdirp'
20
+ import _ignore from 'ignore'
21
+ import { sort } from 'fast-sort'
22
+ import { buildMigrateConfig } from '../migrate-config.js'
23
+ import { loadYAML, dumpYAML } from '../adapters/yaml.js';
24
+ import realFs, { readFileSync } from 'fs'
25
+ import { preparePathForWriting, readFile, writeFile, stat, lstat, unlink } from '../adapters/file-gateway.js'
26
+
27
+ const {
28
+ CONFIG_FILE,
29
+ CONFIG_FILE_YML,
30
+ ASYNC_LIMIT,
31
+ DEFAULT_IGNORE_PATHS,
32
+ DEFAULT_IGNORE_EXTS,
33
+ IGNORE_FILE,
34
+ SORT_FILE,
35
+ ERRORS,
36
+ } = constants
37
+
38
+ const { DEFAULT_CONFIG } = Config
39
+
40
+ export function fileSystemStoreMixin(repo, fs = realFs) {
41
+ repo.allFiles = []
42
+
43
+ function getIgnorePatterns() {
44
+ var ignoreFile = _path.join(repo.path, IGNORE_FILE)
45
+ let patterns = DEFAULT_IGNORE_PATHS
46
+ if (fs.existsSync(ignoreFile))
47
+ patterns = patterns + _eol + fs.readFileSync(ignoreFile).toString()
48
+
49
+ return patterns
50
+ }
51
+
52
+ const IG = _ignore().add(getIgnorePatterns())
53
+
54
+ function shouldIncludeFile(entry) {
55
+ const { path, dirent } = entry
56
+ return !dirent.isSymbolicLink() && !IG.ignores(path)
57
+ }
58
+
59
+ repo.getAllFilePaths = async function (root) {
60
+ const entries = []
61
+ for await (const entry of readdirp(root, { lstat: true, fileFilter: shouldIncludeFile })) {
62
+ entries.push(entry)
63
+ }
64
+ return entries.filter((res) => repo.shouldInclude(res.path))
65
+ .map((res) => res.fullPath)
66
+ }
67
+
68
+ repo.addFilePath = async function (path) {
69
+ var relPath = repo.getRelativePath(path)
70
+ var fullPath = repo.getFullPath(path)
71
+
72
+ const stat = await fs.promises.lstat(fullPath)
73
+ if (stat.isDirectory()) relPath += _path.sep
74
+ if (!repo.allFiles.includes(relPath)) {
75
+ repo.allFiles.push(relPath)
76
+ }
77
+ }
78
+
79
+ repo.removeFilePath = function (path) {
80
+ _remove(repo.allFiles, (item) => item === path)
81
+ }
82
+
83
+ repo.getFilePaths = function () {
84
+ return repo.allFiles
85
+ }
86
+
87
+ repo.migrateConfig = buildMigrateConfig(repo)
88
+ // READY Refactor init to use async/await
89
+ // #esm-migration #important #urgent
90
+ // <!--
91
+ // order:-270
92
+ // -->
93
+ repo.init = async function () {
94
+ const stats = await stat(repo.path)
95
+ if (!stats.isDirectory()) {
96
+ var error = new Error(
97
+ `Path must be an existing directory on the file system: ${repo.path}`
98
+ )
99
+ repo.emit('error', error)
100
+ throw error
101
+ }
102
+
103
+ await repo.loadConfig()
104
+ repo.migrateConfig()
105
+ await repo.saveConfig()
106
+ repo.emit('config.loaded')
107
+ repo.createListeners()
108
+
109
+ const files = await repo.readFiles()
110
+ const lists = repo.getTasksByList()
111
+ repo.emit('fs-store-initialized', { ok: true, lists })
112
+ return files
113
+ }
114
+
115
+ repo.loadIgnore = function () {
116
+ const patterns = getIgnorePatterns() + _eol + DEFAULT_IGNORE_EXTS
117
+ repo.setIgnores(patterns)
118
+ }
119
+
120
+ repo.fileOK = async function (file, includeDirs = false) {
121
+ if (File.isFile(file)) file = file.path
122
+ if (!file || !this.shouldInclude(file)) return false
123
+
124
+ var fullPath = repo.getFullPath(file)
125
+ try {
126
+ const stats = await lstat(fullPath)
127
+
128
+ if (!stats) return false
129
+
130
+ if (/\.\.(\/|\\)/.test(file) || (!includeDirs && stats.isDirectory()))
131
+ return false
132
+
133
+ const content = await readFile(fullPath)
134
+
135
+ if (await isBinaryFile(content, stats.size) ) {
136
+ return false
137
+ }
138
+
139
+ return stats
140
+ } catch (err) {
141
+ return false
142
+ }
143
+ }
144
+
145
+ // READY Refactor saveSort to use async/await
146
+ // #esm-migration #important
147
+ // TODO readSort and saveSort are no longer used
148
+ repo.saveSort = async function (sort) {
149
+ writeFile(repo.getFullPath(SORT_FILE), JSON.stringify(sort), 'utf8')
150
+ }
151
+
152
+ repo.readSort = async function () {
153
+ const data = readFile(repo.getFullPath(SORT_FILE))
154
+ repo.sort = JSON.parse(data)
155
+ }
156
+
157
+ repo.saveConfig = async function () {
158
+ try {
159
+ repo.savingConfig = true
160
+ const file = repo.getFullPath(CONFIG_FILE_YML)
161
+ const data = { ...repo.getConfig() }
162
+ delete data.cardActionsFunction
163
+ delete data.boardActionsFunction
164
+ delete data.cardPropertiesFunction
165
+ delete data.path
166
+ if (data.lists) data.lists.forEach((list) => delete list.tasks)
167
+ const yaml = dumpYAML(data)
168
+ await preparePathForWriting(file)
169
+ await writeFile(file, yaml)
170
+ } finally {
171
+ repo.savingConfig = false
172
+ }
173
+ }
174
+
175
+ repo.updateConfig = async function (loadedConfig) {
176
+ var baseConfig = _cloneDeep(DEFAULT_CONFIG)
177
+ var include_lists = baseConfig.code.include_lists
178
+ const currentConfig = repo.config
179
+ if (currentConfig && currentConfig.code)
180
+ include_lists = _union(include_lists, currentConfig.code.include_lists)
181
+
182
+ if (currentConfig && currentConfig.settings)
183
+ loadedConfig.settings = {...currentConfig.settings, ...loadedConfig.settings}
184
+
185
+ if (loadedConfig && loadedConfig.code)
186
+ include_lists = _union(include_lists, loadedConfig.code.include_lists)
187
+
188
+ const newConfig = new Config(
189
+ _assign({}, baseConfig, repo.config, loadedConfig, { path: repo.path })
190
+ )
191
+
192
+ await repo.migrateTasksByConfig(currentConfig, newConfig)
193
+ repo.config = newConfig
194
+ repo.config.code.include_lists = include_lists
195
+ var _languages = _cloneDeep(languages)
196
+ repo.languages = repo.config.languages
197
+ ? _assign(_languages, repo.config.languages)
198
+ : _languages
199
+ return repo.config
200
+ }
201
+
202
+ // READY Refactor loadConfig to use async/await
203
+ // #esm-migration
204
+ // <!--
205
+ // order:-255
206
+ // -->
207
+ repo.loadConfig = async function (cb) {
208
+ // BACKLOG If a config is bad move it to config.json.bak and save a new one with defaults gh:2 id:9 +enhancement +standup +chore
209
+ // <!--
210
+ // order:-670
211
+ // -->
212
+ repo.loadIgnore()
213
+ const configData = (!repo.config || repo.config.dirty)
214
+ ? await this.getYamlConfig()
215
+ : repo.config || {}
216
+
217
+ return await repo.updateConfig(configData)
218
+ }
219
+
220
+ repo.getConfigFile = function () {
221
+ return repo.getFullPath(CONFIG_FILE)
222
+ }
223
+
224
+ repo.getYamlConfig = async function () {
225
+ const filePath = repo.getFullPath(CONFIG_FILE_YML)
226
+ if (!fs.existsSync(filePath)) return
227
+ const configData = await readFile(filePath, 'utf-8')
228
+ let configLike = {}
229
+ try {
230
+ configLike = loadYAML(configData.toString())
231
+ } catch (e) {
232
+ /* noop */
233
+ }
234
+ return configLike
235
+ }
236
+
237
+ // READY Refactor writeFile to use async/await
238
+ // #esm-migration #important
239
+ // <!--
240
+ // order:-280
241
+ // -->
242
+ repo.writeFile = async (file, emitFileUpdate) => {
243
+ if (!File.isFile(file)) throw new Error(ERRORS.NOT_A_FILE)
244
+ if (file.deleted) return file
245
+
246
+ if (/\.\.(\/|\\)/.test(file.path)) throw new Error('Unable to write file:' + file.path)
247
+
248
+ const filePath = repo.getFullPath(file)
249
+
250
+ await preparePathForWriting(filePath)
251
+
252
+ const oldChecksum = file.checksum
253
+ file.checksum = computeChecksum(file.getContentForFile())
254
+
255
+ try {
256
+ await writeFile(filePath, file.getContentForFile(), 'utf8')
257
+ } catch (err) {
258
+ file.checksum = oldChecksum
259
+ throw new Error('Unable to write file:' + filePath, {cause: err})
260
+ }
261
+
262
+ try {
263
+ const stats = await stat(filePath)
264
+ file.setModifiedTime(stats.mtime)
265
+ file.modified = false
266
+ repo.addFilePath(filePath)
267
+ if (emitFileUpdate) repo.emit('file.saved', file)
268
+ } catch (err) {
269
+ throw new Error('Unable to stat file:' + filePath, {cause: err})
270
+ }
271
+
272
+ return file
273
+ }
274
+
275
+ // READY Refactor getFilesInPath to use async/await
276
+ // #esm-migration #important #urgent
277
+ // <!--
278
+ // order:-20
279
+ // -->
280
+ repo.getFilesInPath = async function (includeDirs) {
281
+ const allPaths = await repo.getAllFilePaths(repo.path)
282
+ const files = []
283
+ let processed = 0
284
+ log('allPaths=', allPaths)
285
+ if (allPaths.length === 0) return files
286
+ await eachLimit(
287
+ allPaths,
288
+ ASYNC_LIMIT,
289
+ async (path) => {
290
+ if (!path) return
291
+ path = repo.getRelativePath(path)
292
+ const stat = await repo.fileOK(path, includeDirs)
293
+ processed++
294
+ repo.emit('file.processed', {
295
+ file: path,
296
+ ok: stat !== false,
297
+ total: allPaths.length,
298
+ processed: processed + 1,
299
+ repoId: repo.getId(),
300
+ })
301
+
302
+ if (stat) {
303
+ log('%s is ok %j', path, stat)
304
+ var file = new File({
305
+ repoId: repo.getId(),
306
+ filePath: path,
307
+ modifiedTime: stat.mtime,
308
+ createdTime: stat.birthtime,
309
+ languages: repo.languages,
310
+ project: repo.project,
311
+ })
312
+ file.isDir = stat.isDirectory()
313
+ files.push(file)
314
+ }
315
+ log('stat=%j', stat, null)
316
+ log('processed=%d allPaths.length=%d', processed, allPaths.length)
317
+ })
318
+ return sort(files).asc(u => u.path)
319
+ }
320
+
321
+ // READY Refactor readFileContent to use async/await
322
+ // #esm-migration #important #urgent
323
+ // <!--
324
+ // order:-40
325
+ // -->
326
+ repo.readFileContent = async function (file) {
327
+ if (!File.isFile(file)) throw new Error(ERRORS.NOT_A_FILE)
328
+ var filePath = repo.getFullPath(file)
329
+
330
+ const stats = await stat(filePath)
331
+ if (!stats.isFile()) return file
332
+ const data = await readFile(filePath, 'utf8')
333
+
334
+ file
335
+ .setContentFromFile(data)
336
+ .setModifiedTime(stats.mtime)
337
+ .setCreatedTime(stats.birthtime)
338
+
339
+ file.lineCount = eol.split(data).length
340
+ return file
341
+ }
342
+
343
+ // READY Refactor deleteFile to use async/await
344
+ // #esm-migration #important
345
+ repo.deleteFile = async function (path) {
346
+ const file = File.isFile(path) ? path : repo.getFile(path)
347
+ if (!file) throw new Error('Unable to delete:' + path)
348
+
349
+ try {
350
+ await unlink(repo.getFullPath(file))
351
+ repo.removeFile(file)
352
+ } catch (err) {
353
+ throw new Error('Unable to delete file:' + path, {cause: err})
354
+ }
355
+
356
+ }
357
+
358
+ return repo
359
+ }
@@ -0,0 +1,206 @@
1
+ import { fileSystemStoreMixin } from './repo-fs-store.js'
2
+ import debug from 'debug'
3
+ const log = debug('imdone-mixins:repo-watched-fs-store')
4
+ import { File } from '../file.js'
5
+ import { constants } from '../constants.js'
6
+ import chokidar from 'chokidar'
7
+ import realFs from 'fs'
8
+ import { logger } from '../adapters/logger.js'
9
+
10
+ // BACKLOG Only emit file.update if the file checksum has changed
11
+ // <!--
12
+ // order:-650
13
+ // -->
14
+ export function watchedFileSystemStoreMixin(repo, fs = realFs) {
15
+ repo = fileSystemStoreMixin(repo, fs)
16
+ const _refresh = repo.refresh
17
+ const _destroy = repo.destroy
18
+ const _init = repo.init
19
+ let initializing = false
20
+ let refreshing = false
21
+
22
+ function ready() {
23
+ return !initializing && !refreshing
24
+ }
25
+
26
+ async function waitForReady() {
27
+ while (!ready()) {
28
+ logger.log('Waiting for repo to be ready:', repo.path)
29
+ await new Promise(resolve => setTimeout(resolve, 500))
30
+ }
31
+ }
32
+
33
+ // READY Refactor init to async/await
34
+ // #esm-migration #important
35
+ // <!--
36
+ // order:-310
37
+ // -->
38
+ repo.init = async function () {
39
+ if (initializing) {
40
+ throw new Error("Can't initialize repo while it's initializing.")
41
+ }
42
+ log('Initializing repo:', repo.path)
43
+ initializing = true
44
+ try {
45
+ const files = await _init.call(repo)
46
+ await repo.initWatcher()
47
+ repo.emit('watching', { ok: true, lists: repo.getLists() })
48
+ initializing = false
49
+ return files
50
+ } catch (err) {
51
+ initializing = false
52
+ throw new Error(`Error initializing repo: ${repo.path}`, { cause: err })
53
+ }
54
+ }
55
+
56
+ repo.destroy = async function () {
57
+ if (repo.watcher) await repo.watcher.close()
58
+ await _destroy.apply(repo)
59
+ }
60
+
61
+ // READY Refactor refresh to async/await
62
+ // #esm-migration #important
63
+ // <!--
64
+ // order:-330
65
+ // -->
66
+ repo.refresh = async function () {
67
+ await waitForReady()
68
+ log('Refreshing repo:', repo.path)
69
+ refreshing = true
70
+ try {
71
+ const files = await _refresh.call(repo)
72
+ await repo.initWatcher()
73
+ refreshing = false
74
+ return files
75
+ } catch (err) {
76
+ refreshing = false
77
+ throw new Error(`Error refreshing repo: ${repo.path}`, { cause: err })
78
+ }
79
+
80
+ }
81
+
82
+ function _isImdoneConfig(path) {
83
+ return path === constants.CONFIG_FILE_YML
84
+ }
85
+
86
+ function _isImdoneIgnore(path) {
87
+ return path === constants.IGNORE_FILE
88
+ }
89
+
90
+ async function onConfigChange(file) {
91
+ logger.info('Loading config. Observed a change to:', file)
92
+ const label = `Refresh repo: ${repo.path}`
93
+ logger.time(label)
94
+ try {
95
+ await repo.refresh()
96
+ repo.emitConfigUpdate(file, repo.config)
97
+ } finally {
98
+ logger.timeEnd(label)
99
+ }
100
+ }
101
+
102
+ const onIgnoreChange = onConfigChange
103
+
104
+ // READY Refactor initWatcher to async/await
105
+ // #esm-migration #important
106
+ // <!--
107
+ // order:-320
108
+ // defer:2025-03-18
109
+ // -->
110
+ repo.initWatcher = async () => {
111
+ return new Promise(async (resolve, reject) => {
112
+ logger.log('initializing watcher for:', repo.path)
113
+ repo.watcher = chokidar.watch(repo.path, {
114
+ persistent: true,
115
+ usePolling: false,
116
+ alwaysStat: true,
117
+ awaitWriteFinish: true,
118
+ atomic: true,
119
+ ignoreInitial: true,
120
+ ignored: (file) => {
121
+ const ignored = !repo.shouldInclude(file)
122
+ if (ignored) logger.log('Ignoring file:', file)
123
+ return ignored
124
+ }
125
+ }).on('error', (err) => {
126
+ logger.error('Error in watcher')
127
+ logger.error(err)
128
+ repo.emit('watch.error', err)
129
+ reject(err)
130
+ })
131
+ .on('ready', () => {
132
+ logger.log('watcher ready for:', repo.path)
133
+ initializing = false
134
+ resolve()
135
+ })
136
+ .on('add', async function (path, stat) {
137
+ logger.log('Watcher received add event for file: ' + path)
138
+ if (stat.isDirectory()) return
139
+ let file = repo.getFile(path)
140
+ if (file === undefined) {
141
+ log('Creating file: ' + path)
142
+ file = new File({
143
+ repoId: repo.getId(),
144
+ filePath: path,
145
+ languages: repo.languages,
146
+ project: repo.project,
147
+ })
148
+ }
149
+
150
+ stat = await repo.fileOK()
151
+ log('fileOK returned: ' + JSON.stringify(stat))
152
+ if (stat.mtime <= file.getModifiedTime()) {
153
+ return log('File update already applied. Change ignored for file: ' + path)
154
+ }
155
+ log('Reading file: ' + path)
156
+ file = await repo.readFile(file)
157
+ repo.emi('watch.add', file)
158
+ repo.emitFileUpdate(file)
159
+ })
160
+ .on('change', async function (path, stat) {
161
+ logger.log(
162
+ `Watcher received change event for file: ${path} repoPath: ${repo.path}`,
163
+ stat.mtime
164
+ )
165
+ if (!path || stat.isDirectory()) return
166
+ var file = repo.getFile(path) || path
167
+ const isFile = File.isFile(file)
168
+ // BACKLOG ## Handle the case where the file is not in the repo
169
+ // #urgent #1.12.0
170
+ // <!--
171
+ // order:-660
172
+ // -->
173
+ if (isFile && stat.mtime <= file.getModifiedTime()) {
174
+ // logger.log("File update already applied. Change ignored.")
175
+ return
176
+ }
177
+ if (_isImdoneConfig(path)) return onConfigChange(path)
178
+ if (_isImdoneIgnore(path)) return onIgnoreChange(path)
179
+
180
+ const ok = await repo.fileOK((file && file.path) || path)
181
+ if (!ok) return
182
+ log('Reading file: ' + path)
183
+ if (!isFile) file = repo.getFile(path)
184
+ file = await repo.readFile(file)
185
+ repo.emit('watch.change', file)
186
+ repo.emitFileUpdate(file)
187
+ })
188
+ .on('delete', async function (path) {
189
+ log('Watcher received unlink event for file: ' + path)
190
+ repo.removeFilePath(path)
191
+ var file = new File({
192
+ repoId: repo.getId(),
193
+ filePath: path,
194
+ languages: repo.languages,
195
+ project: repo.project,
196
+ })
197
+ log('Removing file: ' + path)
198
+ repo.removeFile(file)
199
+ repo.emit('watch.delete', file)
200
+ repo.emitFileUpdate(file, true)
201
+ })
202
+ });
203
+ }
204
+
205
+ return repo
206
+ }
@@ -0,0 +1,72 @@
1
+ import Plugin from 'imdone-api'
2
+ import path from 'path'
3
+ import { getRawTask } from '../adapters/parsers/task/CardContentParser.js'
4
+
5
+ export default class ArchivePlugin extends Plugin {
6
+ constructor(project) {
7
+ super(project)
8
+ }
9
+
10
+ get config() {
11
+ return this.project.config
12
+ }
13
+
14
+ get fileGateway() {
15
+ return this.project.fileGateway
16
+ }
17
+
18
+ async onBeforeBoardUpdate() {
19
+ const config = this.config
20
+ if (!config || !config.archiveCompleted || this.archivingTasks) return
21
+ this.archivingTasks = true
22
+ const cards = this.project.getAllCards(`meta.archived != "true" and list = ${config.doneList}`)
23
+ const messageUser = cards.length > 1
24
+
25
+ if (!cards.length) {
26
+ this.archivingTasks = false
27
+ return
28
+ } else {
29
+ if (messageUser) this.project.snackBar({message: `Archiving ${cards.length} card(s)...`})
30
+ }
31
+
32
+ for (const card of cards) {
33
+ await this.archiveTask(card)
34
+ }
35
+
36
+ if (messageUser) this.project.snackBar({message: ` Done archiving ${cards.length} card(s). Deleting original(s)...`})
37
+ await this.project.deleteTasks(cards)
38
+
39
+ this.archivingTasks = false
40
+ if (messageUser) this.project.snackBar({message: `Done deleting original card(s).`})
41
+
42
+ }
43
+
44
+ async archiveTask (task, config = this.config) {
45
+ const archiveFolder = config.archiveFolder
46
+ const fileDir = path.dirname(task.relPath)
47
+
48
+ if (fileDir.startsWith(archiveFolder)) return
49
+
50
+ const fileName = this.fileGateway.sanitizeFileName(`${task.text}.md`, config.replaceSpacesWith)
51
+ const newPath = path.join(this.project.path, archiveFolder, fileDir, fileName)
52
+
53
+ const content = this.project.addMetaToContent(
54
+ [
55
+ { key: 'archived', value: 'true'},
56
+ { key: 'archivedAt', value: this.project.isoDateWithOffset },
57
+ { key: 'originalPath', value: task.relPath },
58
+ { key: 'originalLine', value: task.line }
59
+ ],
60
+ task.content
61
+ )
62
+
63
+ const { tokenPrefix } = config
64
+ const { beforeText, list } = task
65
+ const taskStart = getRawTask({tokenPrefix, beforeText, list, text: ''})
66
+ const taskContent = `${taskStart} ${content}\n\n\n`
67
+
68
+ await this.fileGateway.preparePathForWriting(newPath)
69
+ await this.fileGateway.appendFileSync(newPath, taskContent)
70
+ }
71
+
72
+ }
@@ -0,0 +1,56 @@
1
+ import Plugin from 'imdone-api'
2
+
3
+ export default class DefaultBoardActionsPlugin extends Plugin {
4
+ constructor(project) {
5
+ super(project)
6
+ }
7
+
8
+ getBoardActions() {
9
+ const project = this.project
10
+ const isWindows = process.platform === 'win32'
11
+ const openIn = isWindows ? 'Explorer' : 'Finder'
12
+
13
+ return [
14
+ {
15
+ title: 'Duplicate card',
16
+ keys: ['shift+d'],
17
+ icon: 'clone',
18
+ action: async (task) => {
19
+ const timestamp = project.isoDateWithOffset
20
+ await project.newCard({
21
+ list: project.config.getDefaultList(),
22
+ path: task.relPath,
23
+ template: task.content.replace(/(\screated:).*(\s)/, `$1${timestamp}$2`),
24
+ })
25
+ project.toast({ message: `"${task.text}" duplicated` })
26
+ },
27
+ },
28
+ {
29
+ title: 'Copy card to clipboard',
30
+ keys: ['c'],
31
+ icon: 'clone',
32
+ action: (task) => {
33
+ return project.copyToClipboard(task.content, `"${task.text} markdown" copied to clipboard`)
34
+ },
35
+ },
36
+ {
37
+ title: 'Copy card title to clipboard',
38
+ keys: ['mod+t'],
39
+ icon: 'clone',
40
+ action: (task) => {
41
+ const title = project.renderMarkdown(task.text, task.fullPath).replaceAll(/<\/*p>/gi, '')
42
+ return project.copyToClipboard(title, `"${title}" copied to clipboard`)
43
+ },
44
+ },
45
+ {
46
+ title: `Open in ${openIn}`,
47
+ keys: ['alt+shift+o'],
48
+ icon: "folder",
49
+ async action () {
50
+ await project.openPath(project.path)
51
+ }
52
+ }
53
+ ]
54
+ }
55
+
56
+ }