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.
- package/.dependency-cruiser.cjs +412 -0
- package/.github/ISSUE_TEMPLATE/bug_report.md +37 -0
- package/.github/workflows/ci.yml +24 -0
- package/.imdoneignore +13 -0
- package/.mocharc.json +10 -0
- package/.prettierrc +6 -0
- package/CHANGELOG.md +160 -0
- package/_index.md +7 -0
- package/_navbar.md +1 -0
- package/build.js +57 -0
- package/esm-migration.md +446 -0
- package/index.html +184 -0
- package/lib/FileProjectContext.js +13 -0
- package/lib/ProjectContext.js +43 -0
- package/lib/adapters/date-time.js +15 -0
- package/lib/adapters/environment.js +3 -0
- package/lib/adapters/file-gateway.js +154 -0
- package/lib/adapters/git-download.js +55 -0
- package/lib/adapters/index.js +9 -0
- package/lib/adapters/logger.js +136 -0
- package/lib/adapters/markdown.js +180 -0
- package/lib/adapters/parsers/DateLanguageParser.js +12 -0
- package/lib/adapters/parsers/content-transformer.js +116 -0
- package/lib/adapters/parsers/file/LocalFileParserFactory.js +36 -0
- package/lib/adapters/parsers/file/ReadStreamCodeParser.js +126 -0
- package/lib/adapters/parsers/file/ReadStreamMarkdownParser.js +17 -0
- package/lib/adapters/parsers/file/ReadStreamTaskParser.js +106 -0
- package/lib/adapters/parsers/file/index.js +4 -0
- package/lib/adapters/parsers/function-parser.js +25 -0
- package/lib/adapters/parsers/index.js +5 -0
- package/lib/adapters/parsers/task/CardContentParser.js +367 -0
- package/lib/adapters/parsers/task/CheckStyleTaskParser.js +28 -0
- package/lib/adapters/parsers/task/CodeStyleTaskParser.js +24 -0
- package/lib/adapters/parsers/task/HashStyleTaskParser.js +27 -0
- package/lib/adapters/parsers/task/MarkdownStyleTaskParser.js +24 -0
- package/lib/adapters/parsers/task/TaskParser.js +38 -0
- package/lib/adapters/parsers/task/index.js +6 -0
- package/lib/adapters/storage/config.js +44 -0
- package/lib/adapters/storage/index.js +2 -0
- package/lib/adapters/storage/tags.js +29 -0
- package/lib/adapters/yaml.js +9 -0
- package/lib/card.js +299 -0
- package/lib/checksum.js +22 -0
- package/lib/config.js +249 -0
- package/lib/constants.js +68 -0
- package/lib/context/ApplicationContext.js +35 -0
- package/lib/context/index.js +1 -0
- package/lib/default-settings.js +37 -0
- package/lib/domain/entities/View.js +23 -0
- package/lib/domain/entities/index.js +1 -0
- package/lib/domain/index.js +2 -0
- package/lib/domain/usecases/GetViewsUsecase.js +44 -0
- package/lib/domain/usecases/SaveViewUsecase.js +51 -0
- package/lib/domain/usecases/index.js +2 -0
- package/lib/file.js +1391 -0
- package/lib/get-tasks-to-modify.js +117 -0
- package/lib/languages.js +113 -0
- package/lib/list.js +56 -0
- package/lib/migrate-config.js +165 -0
- package/lib/mixins/index.js +2 -0
- package/lib/mixins/repo-fs-store.js +359 -0
- package/lib/mixins/repo-watched-fs-store.js +206 -0
- package/lib/plugins/archive-plugin.js +72 -0
- package/lib/plugins/default-board-actions-plugin.js +56 -0
- package/lib/plugins/default-board-properties-plugin.js +101 -0
- package/lib/plugins/default-plugins.js +7 -0
- package/lib/plugins/epic-plugin.js +177 -0
- package/lib/plugins/extension-plugin.js +94 -0
- package/lib/plugins/index.js +9 -0
- package/lib/plugins/persist-tags-plugin.js +14 -0
- package/lib/plugins/plugin-manager.js +458 -0
- package/lib/plugins/plugin-registry.js +57 -0
- package/lib/project-factory.js +42 -0
- package/lib/project.js +864 -0
- package/lib/repository.js +1514 -0
- package/lib/task.js +780 -0
- package/lib/tools.js +176 -0
- package/lib/usecases/actions.js +32 -0
- package/lib/usecases/get-imdone-url.js +6 -0
- package/lib/usecases/get-project-tags.js +7 -0
- package/lib/usecases/get-tasks-in-file.js +26 -0
- package/lib/usecases/index.js +4 -0
- package/package.json +2 -6
- package/scripts/delete-markdown-indexes.js +87 -0
- package/scripts/generate-index.js +35 -0
- package/scripts/index-markdown.js +55 -0
- package/tsconfig.json +15 -0
- package/types/FileProjectContext.d.ts +4 -0
- package/types/ProjectContext.d.ts +9 -0
- package/types/adapters/date-time.d.ts +2 -0
- package/types/adapters/environment.d.ts +1 -0
- package/types/adapters/file-gateway.d.ts +51 -0
- package/types/adapters/git-download.d.ts +1 -0
- package/types/adapters/index.d.ts +9 -0
- package/types/adapters/logger.d.ts +24 -0
- package/types/adapters/markdown.d.ts +13 -0
- package/types/adapters/parsers/DateLanguageParser.d.ts +1 -0
- package/types/adapters/parsers/content-transformer.d.ts +10 -0
- package/types/adapters/parsers/file/LocalFileParserFactory.d.ts +5 -0
- package/types/adapters/parsers/file/ReadStreamCodeParser.d.ts +13 -0
- package/types/adapters/parsers/file/ReadStreamMarkdownParser.d.ts +7 -0
- package/types/adapters/parsers/file/ReadStreamTaskParser.d.ts +19 -0
- package/types/adapters/parsers/file/index.d.ts +4 -0
- package/types/adapters/parsers/function-parser.d.ts +1 -0
- package/types/adapters/parsers/index.d.ts +5 -0
- package/types/adapters/parsers/task/CardContentParser.d.ts +45 -0
- package/types/adapters/parsers/task/CheckStyleTaskParser.d.ts +12 -0
- package/types/adapters/parsers/task/CodeStyleTaskParser.d.ts +14 -0
- package/types/adapters/parsers/task/HashStyleTaskParser.d.ts +13 -0
- package/types/adapters/parsers/task/MarkdownStyleTaskParser.d.ts +14 -0
- package/types/adapters/parsers/task/TaskParser.d.ts +7 -0
- package/types/adapters/parsers/task/index.d.ts +6 -0
- package/types/adapters/storage/config.d.ts +6 -0
- package/types/adapters/storage/index.d.ts +2 -0
- package/types/adapters/storage/tags.d.ts +3 -0
- package/types/adapters/yaml.d.ts +2 -0
- package/types/card.d.ts +166 -0
- package/types/checksum.d.ts +8 -0
- package/types/config.d.ts +61 -0
- package/types/constants.d.ts +46 -0
- package/types/context/ApplicationContext.d.ts +24 -0
- package/types/context/index.d.ts +1 -0
- package/types/default-settings.d.ts +2 -0
- package/types/domain/entities/FileProjectContext.d.ts +5 -0
- package/types/domain/entities/View.d.ts +17 -0
- package/types/domain/entities/index.d.ts +1 -0
- package/types/domain/index.d.ts +2 -0
- package/types/domain/usecases/GetViewsUsecase.d.ts +4 -0
- package/types/domain/usecases/SaveViewUsecase.d.ts +8 -0
- package/types/domain/usecases/index.d.ts +2 -0
- package/types/file.d.ts +344 -0
- package/types/get-tasks-to-modify.d.ts +3 -0
- package/types/index.d.ts +23 -0
- package/types/languages.d.ts +649 -0
- package/types/list.d.ts +30 -0
- package/types/migrate-config.d.ts +1 -0
- package/types/mixins/index.d.ts +2 -0
- package/types/mixins/repo-fs-store.d.ts +2 -0
- package/types/mixins/repo-watched-fs-store.d.ts +2 -0
- package/types/plugins/archive-plugin.d.ts +8 -0
- package/types/plugins/default-board-actions-plugin.d.ts +9 -0
- package/types/plugins/default-board-properties-plugin.d.ts +16 -0
- package/types/plugins/default-plugins.d.ts +6 -0
- package/types/plugins/epic-plugin.d.ts +19 -0
- package/types/plugins/extension-plugin.d.ts +15 -0
- package/types/plugins/index.d.ts +9 -0
- package/types/plugins/persist-tags-plugin.d.ts +4 -0
- package/types/plugins/plugin-manager.d.ts +68 -0
- package/types/plugins/plugin-registry.d.ts +1 -0
- package/types/project-factory.d.ts +14 -0
- package/types/project.d.ts +158 -0
- package/types/repository.d.ts +996 -0
- package/types/task.d.ts +202 -0
- package/types/tools.d.ts +29 -0
- package/types/usecases/actions.d.ts +7 -0
- package/types/usecases/get-imdone-url.d.ts +1 -0
- package/types/usecases/get-project-tags.d.ts +1 -0
- package/types/usecases/get-tasks-in-file.d.ts +4 -0
- package/types/usecases/index.d.ts +4 -0
- 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
|
+
}
|