imdone-core 1.45.0 → 1.46.0

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.
@@ -1,327 +0,0 @@
1
- const ApplicationContext = require('./adapters/ApplicationContext')
2
- const {simpleGit} = require('simple-git')
3
- const prompts = require('prompts')
4
- const eol = require('eol')
5
- const chalk = require('chalk')
6
- const BacklogProject = require('./domain/BacklogProject')
7
- const StoryProject = require('./domain/StoryProject')
8
- const { createFileSystemProject } = require('../project-factory.js')
9
- const commandExists = require('command-exists')
10
- const git = simpleGit()
11
- const _path = require('path')
12
- const {
13
- importMarkdown,
14
- } = require('./MDStoryParser')
15
-
16
- let consoleLog = () => {}
17
- const applicationContext = () => {
18
- return {
19
- ...ApplicationContext(),
20
- log: consoleLog
21
- }
22
- }
23
-
24
- const {
25
- configureProject,
26
- defaultProjectPath,
27
- projectExists,
28
- } = require('./ProjectConfigFactory')
29
-
30
- const {promptForMarkdownPlan} = require('./prompts/MarkdownPlanPrompt.js')
31
- const {
32
- TASK_ID,
33
- UNGROUPED_TASKS
34
- } = BacklogProject.constants
35
-
36
- const replacer = (key, value) =>
37
- value instanceof Object && !(value instanceof Array) ?
38
- Object.keys(value)
39
- .sort()
40
- .reduce((sorted, key) => {
41
- sorted[key] = value[key];
42
- return sorted
43
- }, {}) :
44
- value;
45
-
46
- module.exports = CliController = {}
47
-
48
- CliController.exportProject = async function (log, verbose = false) {
49
- const project = createFileSystemProject({path: process.env.PWD})
50
- await project.init()
51
- let jsonOutput = verbose
52
- ? project.toImdoneJSON()
53
- : (() => {
54
- const result = {}
55
- const imdoneJSON = project.toImdoneJSON()
56
- const lists = imdoneJSON.lists.map(list => {
57
- list.tasks = list.tasks.map(task => {
58
- const result = {
59
- created: task.created,
60
- completed: task.completed,
61
- due: task.due,
62
- path: _path.join(task.source.repoId, task.source.path),
63
- meta: task.allMeta,
64
- tags: task.allTags,
65
- context: task.allContext,
66
- line: task.line,
67
- content: task.content,
68
- interpretedContent: task.interpretedContent,
69
- lastLine: task.lastLine,
70
- list: task.list,
71
- progress: task.progress,
72
- order: task.order,
73
- beforeText: task.beforeText,
74
- props: task.props,
75
- }
76
- return result
77
- })
78
- return list
79
- })
80
- result.lists = lists
81
- result.totals = imdoneJSON.totals
82
- result.tags = imdoneJSON.tags
83
-
84
- return result
85
- })()
86
-
87
- log(JSON.stringify(jsonOutput, replacer, 3))
88
- }
89
-
90
- CliController.imdoneInit = async function () {
91
- if (await projectExists(defaultProjectPath())) {
92
- throw new Error('Imdone backlog is already initialized!')
93
- }
94
- const defaultBranch = await promptForDefaultBranch()
95
- const remote = await promptForRemote()
96
- await configureProject({defaultBranch, remote})
97
- }
98
-
99
- CliController.openBoard = async function (log) {
100
- const open = (await import('open')).default
101
- try {
102
- await commandExists('imdone')
103
- const { storyProject, task } = await getCurrentTask()
104
- const taskId = task && task.meta[TASK_ID] && task.meta[TASK_ID][0]
105
-
106
- if (storyProject) {
107
- const queryString = taskId ? `?filter=meta.${TASK_ID}="${taskId}"` : ''
108
- open(`imdone://${storyProject.path}${queryString}`)
109
- } else {
110
- open(`imdone://${defaultProjectPath()}`)
111
- }
112
- } catch (e) {
113
- console.error(e)
114
- log(`\nimdone is not installed! Visit ${chalk.green('https://imdone.io')} to install imdone.`)
115
- open(`https://imdone.io`)
116
- }
117
- }
118
-
119
- CliController.openTaskFile = async function (log) {
120
- const open = (await import('open')).default
121
- let { task } = await getCurrentTask()
122
- if (!task) {
123
- const storyId = await promptForStoryId()
124
- const taskId = await promptForTaskId(storyId)
125
- const storyProject = await StoryProject.createAndInit(defaultProjectPath(), null, storyId, applicationContext)
126
- task = storyProject.getTask(taskId) || storyProject.getStory()
127
- }
128
-
129
- open(task.fullPath)
130
- }
131
-
132
-
133
- // TODO: This should ask for a group to add a task to
134
- CliController.addTask = async function ({content, storyId, group, log}) {
135
- consoleLog = log
136
- storyId = await determineStoryId(storyId)
137
- const storyProject = await StoryProject.createAndInit(defaultProjectPath(), null, storyId, applicationContext)
138
-
139
- const task = await storyProject.addTask({group, content})
140
-
141
- log(`${TASK_ID}:"${getTaskId(task)}"`)
142
- }
143
-
144
- CliController.listTasks = async function ({storyId, filter=null, json, log}) {
145
- consoleLog = log
146
- storyId = storyId || await determineStoryId(storyId)
147
-
148
- const storyProject = await StoryProject.createAndInit(defaultProjectPath(), null, storyId, applicationContext)
149
-
150
- const tasks = storyProject.getTasks(filter)
151
-
152
- if (json) return log(
153
- JSON.stringify({
154
- name: storyProject.name,
155
- path: storyProject.path,
156
- tasks: storyProject.tasksToJson(tasks)
157
- }, null, 2)
158
- )
159
-
160
- const story = storyProject.getStory()
161
- const groupedTasks = getGroupedTasks(tasks)
162
- logStory(log, storyProject, story)
163
- logGroupedTasks(log, storyProject, groupedTasks)
164
- }
165
-
166
- CliController.planStory = async function(markdown, storyId, log = console.log) {
167
- consoleLog = log
168
- if (storyId) {
169
- storyId = await determineStoryId(storyId)
170
- }
171
- if (!markdown) {
172
- markdown = await promptForMarkdownPlan()
173
- }
174
- return await importMarkdown({
175
- projectPath: defaultProjectPath(),
176
- existingStoryId: storyId,
177
- markdown,
178
- applicationContext
179
- })
180
- }
181
-
182
- CliController.startTask = async function (taskId, log) {
183
- consoleLog = log
184
- if (!taskId) {
185
- const storyId = await determineStoryId()
186
- taskId = await promptForTaskId(storyId)
187
- }
188
-
189
- if (!taskId) return log(chalk.bgRed('No tasks found!'))
190
- const project = await BacklogProject.createAndInit(defaultProjectPath(), null, applicationContext)
191
- await project.startTask(taskId)
192
- }
193
-
194
- CliController.completeTask = async function (log) {
195
- consoleLog = log
196
- const {storyProject, task} = await getCurrentTask()
197
- if (!task) return noTaskStarted(log)
198
- try {
199
- const task = await storyProject.completeTask()
200
- log()
201
- logTask(log, storyProject, task)
202
- log(chalk.bgGreen('Remember to commit and push!'))
203
- } catch (e) {
204
- log(chalk.bgRed(e.message))
205
- }
206
- }
207
-
208
- function noTaskStarted(log) {
209
- log('\n', chalk.bgRed('No task started!'))
210
- }
211
- function getTaskId(task) {
212
- const taskIdMeta = task.meta[TASK_ID]
213
- return taskIdMeta ? taskIdMeta[0] : null
214
- }
215
-
216
- async function getCurrentTask() {
217
- const backlogProject = await BacklogProject.createAndInit(defaultProjectPath(), null, applicationContext)
218
- const storyId = backlogProject.getCurrentStoryId()
219
- if (!storyId) return { backlogProject }
220
- const storyProject = await StoryProject.createAndInit(defaultProjectPath(), null, storyId, applicationContext)
221
- const task = await storyProject.getCurrentTask()
222
- return { backlogProject, storyProject, storyId, task }
223
- }
224
-
225
- async function determineStoryId(storyId) {
226
- const project = await BacklogProject.createAndInit(defaultProjectPath(), null, applicationContext)
227
- storyId = storyId || project.getCurrentStoryId()
228
- storyId = await promptForStoryId(storyId)
229
- return storyId
230
- }
231
-
232
- async function promptForStoryId(lastStoryId) {
233
- const backlogProject = new BacklogProject(defaultProjectPath(), null, applicationContext)
234
- await backlogProject.init()
235
-
236
- const storyIds = backlogProject.getStoryIds().filter(({list}) => list !== backlogProject.config.getDoneList())
237
- const choices = storyIds.map(({storyId, text}) => ({ title: `${text} [ ${storyId} ]`, value: storyId }))
238
- let initial = storyIds.findIndex(({storyId}) => storyId === lastStoryId)
239
- if (initial < 0) initial = 0
240
- const result = await prompts({
241
- type: 'select',
242
- name: 'storyId',
243
- message: 'Select a story',
244
- choices,
245
- initial
246
- })
247
- return result.storyId
248
- }
249
-
250
- async function promptForTaskId(storyId) {
251
- const storyProject = await StoryProject.createAndInit(defaultProjectPath(), null, storyId, applicationContext)
252
- const tasks = storyProject.getTasks(`list != ${storyProject.config.getDoneList()}`)
253
- const choices = tasks.map(({text, meta}) => ({ title: `${text} [ ${meta[TASK_ID]} ]`, value: meta[TASK_ID] }))
254
- const result = await prompts({
255
- type: 'select',
256
- name: 'taskId',
257
- message: 'Select a task',
258
- choices
259
- })
260
- return result.taskId
261
- }
262
-
263
- async function promptForDefaultBranch() {
264
- const branches = (await git.branchLocal()).all
265
- const initial = branches.includes('master') ? 'master' : 'main'
266
- const result = await prompts({
267
- type: 'text',
268
- name: 'defaultBranch',
269
- message: 'Enter the name of the default branch',
270
- initial
271
- })
272
- return result.defaultBranch
273
- }
274
-
275
- async function promptForRemote() {
276
- const remotes = (await git.getRemotes(true)).map(({name}) => ({title: name, value: name}))
277
- if (remotes.length === 1) return remotes[0].value
278
- if (remotes.length === 0) return
279
-
280
- const result = await prompts({
281
- type: 'select',
282
- name: 'remote',
283
- message: 'Select a remote',
284
- choices: remotes
285
- })
286
- return result.remote
287
- }
288
-
289
- function getGroupedTasks(tasks) {
290
- const groupedTasks = {}
291
- tasks.forEach((task) => {
292
- const group = task.meta.group
293
- if (group) {
294
- if (!groupedTasks[group]) groupedTasks[group] = []
295
- groupedTasks[group].push(task)
296
- }
297
- })
298
- return groupedTasks
299
- }
300
-
301
- function logGroupedTasks(log, project, groupedTasks) {
302
- log('\n## Tasks\n')
303
- Object.keys(groupedTasks).forEach((group) => {
304
- if (group !== UNGROUPED_TASKS) {
305
- log('')
306
- log(`### ${group}`)
307
- log('')
308
- }
309
- logTasks(log, project, groupedTasks[group])
310
- })
311
- }
312
-
313
- function logTasks(log, project, tasks) {
314
- tasks.forEach(task => logTask(log, project, task))
315
- }
316
-
317
- function logStory(log, project, story) {
318
- log(`# ${project.name}`)
319
- log('')
320
- log(`${story.text}`)
321
- log(story.description.filter(ln => !/^<!--(.*?)-->$/.test(ln)).join(String(eol.lf)))
322
- }
323
-
324
- function logTask(log, project, task) {
325
- const doneMark = (task.list === project.config.getDoneList()) ? 'x' : ' '
326
- log(`- [${doneMark}] ${task.text} <!-- task-id:"${getTaskId(task)}" -->`)
327
- }
@@ -1,139 +0,0 @@
1
- const { parseMD } = require('../tools')
2
- const StoryProject = require('./domain/StoryProject')
3
- const BacklogProject = require('./domain/BacklogProject')
4
- const {
5
- UNGROUPED_TASKS,
6
- TODO,
7
- DONE,
8
- ORDER
9
- } = BacklogProject.constants
10
- const ApplicationContext = require('./adapters/ApplicationContext')
11
-
12
- function isOpen(token, type, tag = token.tag) {
13
- return token.type === type + '_open' && token.tag === tag
14
- }
15
-
16
- function isClose(token, type, tag = token.tag) {
17
- return token.type === type + '_close' && token.tag === tag
18
- }
19
-
20
- function getHeadingLevel(tag) {
21
- return parseInt(tag.substring(1), 10)
22
- }
23
-
24
- function parse(markdown) {
25
- const heading = 'heading'
26
- const paragraph = 'paragraph'
27
- const inline = 'inline'
28
- const bulletList = 'bullet_list'
29
- const listItem = 'list_item'
30
- const textType = 'text'
31
- let storyText, description = [], groupName = '', tasks = []
32
- let inHeading = false
33
- let inParagraph = false
34
- let inBulletList = false
35
- let inListItem = false
36
- let inTasks = false
37
- let headingLevel = 0
38
- const ast = parseMD(markdown)
39
-
40
- ast.forEach((token) => {
41
- const { type, tag, markup, content, info, children } = token
42
- if (isOpen(token, heading)) {
43
- inHeading = true
44
- headingLevel = getHeadingLevel(tag)
45
- } else if (isClose(token, heading)) {
46
- inHeading = false
47
- }
48
-
49
- if (isOpen(token, bulletList)) {
50
- inBulletList = true
51
- } else if (isClose(token, bulletList)) {
52
- inBulletList = false
53
- }
54
-
55
- if (isOpen(token, listItem)) {
56
- inListItem = true
57
- } else if (isClose(token, listItem)) {
58
- inListItem = false
59
- }
60
-
61
- if (isOpen(token, paragraph)) {
62
- inParagraph = true
63
- } else if (isClose(token, paragraph)) {
64
- inParagraph = false
65
- }
66
-
67
- if (inHeading && type == inline) {
68
- groupName = content
69
- if (headingLevel == 1) {
70
- storyText = groupName
71
- }
72
- }
73
-
74
- if (groupName.toLowerCase().endsWith('tasks') && headingLevel == 2) {
75
- inTasks = true
76
- }
77
-
78
- if (inTasks && inBulletList && inListItem && inParagraph && type == inline) {
79
- const group = headingLevel > 2 ? groupName : UNGROUPED_TASKS
80
- const done = /^\[x\]\s/.test(content)
81
- const description = children
82
- .filter(child => child.type === textType)
83
- .map(child => child.content)
84
- const text = description.join('\n')
85
- tasks.push({ group, text, done })
86
- }
87
- })
88
-
89
- if (storyText) {
90
- const result = new RegExp(`# ${storyText}\n(.*)## Tasks`, 'gms').exec(markdown)
91
- description = result && result.length > 1 ? result[1].trim() : ''
92
- }
93
-
94
- return { storyText, description, tasks }
95
- }
96
-
97
- async function importMarkdown({projectPath, existingStoryId, markdown, applicationContext = ApplicationContext}) {
98
- const { log } = applicationContext()
99
-
100
- const backlogProject = await BacklogProject.createAndInit(projectPath, null, applicationContext)
101
-
102
- let { storyText, description, tasks } = parse(markdown)
103
-
104
- const storyId = existingStoryId || backlogProject.toStoryId(storyText)
105
-
106
- log(`Importing story ${storyId}\n${description}`)
107
-
108
- await backlogProject.storageAdapter.initStoryPlan(storyId)
109
-
110
- if (!existingStoryId) await backlogProject.addStory(`${storyText}\n${description}`)
111
-
112
- const storyProject = await StoryProject.createAndInit(projectPath, null, storyId, applicationContext)
113
-
114
- await addStoryTasks(storyProject, tasks)
115
-
116
- await storyProject.init()
117
-
118
- await backlogProject.storageAdapter.saveStoryPlan(storyId)
119
-
120
- return storyProject
121
- }
122
-
123
- async function addStoryTasks(storyProject, tasks) {
124
- tasks.forEach(async (task, i) => {
125
- const order = (i + 1) * (10)
126
- const list = task.done ? DONE : TODO
127
- const meta = [
128
- { key: 'group', value: task.group },
129
- { key: ORDER, value: order }
130
- ]
131
- await storyProject.addTaskToFile({list, content: task.text, meta})
132
- })
133
- }
134
-
135
- module.exports = {
136
- UNGROUPED_TASKS,
137
- parse,
138
- importMarkdown
139
- }
@@ -1,114 +0,0 @@
1
- const DefaultApplicationContext = require('./adapters/ApplicationContext')
2
- const { loadYAML, dumpYAML } = require('../adapters/yaml')
3
- const Config = require('../config')
4
- const { readFile, mkdir, cp, rm, writeFile, access } = require('fs').promises
5
- const { resolve } = path = require('path')
6
- const BacklogProject = require('./domain/BacklogProject')
7
- const StoryProject = require('./domain/StoryProject')
8
-
9
- const DEFAULT_PROJECT_DIR = 'backlog'
10
- const STORIES_DIR = 'stories'
11
- const TASKS_DIR = 'tasks'
12
- const CONFIG_PATH = path.join(__dirname, '..', '..', 'devops', 'imdone')
13
- const BACKLOG_CONFIG_PATH = path.join(CONFIG_PATH, 'backlog-config.yml')
14
- const STORY_CONFIG_PATH = path.join(CONFIG_PATH, 'story-config.yml')
15
- const BACKLOG_IGNORE_PATH = path.join(CONFIG_PATH, 'backlog.imdoneignore')
16
-
17
- const defaultProjectPath = (baseDir = process.env.PWD) => path.join(baseDir, DEFAULT_PROJECT_DIR)
18
-
19
- const getProjectConfigPath = (projectPath = defaultProjectPath()) => path.join(projectPath, '.imdone', 'config.yml')
20
-
21
- const projectExists = async (
22
- projectPath = defaultProjectPath()
23
- ) => {
24
- const projectConfigPath = getProjectConfigPath(projectPath)
25
- try {
26
- await access(projectConfigPath)
27
- return projectConfigPath
28
- } catch (e) {
29
- return false
30
- }
31
- }
32
-
33
- const configureProject = async ({
34
- projectPath = defaultProjectPath(),
35
- configPath = BACKLOG_CONFIG_PATH,
36
- tasksDir = STORIES_DIR,
37
- defaultBranch,
38
- remote,
39
- name
40
- }) => {
41
- projectPath = resolve(projectPath)
42
- configPath = resolve(configPath)
43
-
44
- if (await projectExists(projectPath)) {
45
- return { projectPath, config: await newConfigFromFile(projectConfigPath)}
46
- }
47
-
48
- const projectConfigPath = getProjectConfigPath(projectPath)
49
-
50
- await mkdir(path.join(projectPath, tasksDir), {recursive: true})
51
- await mkdir(path.join(projectPath, '.imdone'), { recursive: true })
52
-
53
- const config = await newConfigFromFile(configPath)
54
- const parentDir = path.basename(path.dirname(projectPath))
55
- config.name = name || `${parentDir} ${DEFAULT_PROJECT_DIR}`
56
- config.journalPath = tasksDir
57
- const pluginConfig = config.settings.plugins.CliBacklogPlugin
58
- config.settings.plugins.CliBacklogPlugin = {...pluginConfig, defaultBranch, remote}
59
-
60
- await writeFile(projectConfigPath, dumpYAML(config))
61
- return { projectPath, config }
62
- }
63
-
64
- const newBacklogProject = async ({
65
- projectPath = defaultProjectPath(),
66
- configPath = BACKLOG_CONFIG_PATH,
67
- tasksDir = STORIES_DIR,
68
- ApplicationContext = DefaultApplicationContext
69
- }) => {
70
- try {
71
- await cp(BACKLOG_IGNORE_PATH, path.join(projectPath, '.imdoneignore'))
72
- } catch (e) {
73
- logFsError(ApplicationContext, e)
74
- }
75
-
76
- const { config } = projectConfig = await configureProject({projectPath, configPath, tasksDir})
77
- return new BacklogProject(projectConfig.projectPath, config, ApplicationContext)
78
- }
79
-
80
- const newStoryProject = async ({
81
- projectPath = defaultProjectPath(),
82
- configPath = STORY_CONFIG_PATH,
83
- tasksDir = TASKS_DIR,
84
- storyId,
85
- ApplicationContext = DefaultApplicationContext
86
- }) => {
87
- const storyProjectPath = BacklogProject.getStoryProjectPath(projectPath, storyId)
88
- try {
89
- await rm(storyProjectPath, { recursive: true })
90
- await mkdir(storyProjectPath, { recursive: true })
91
- } catch (e) {
92
- logFsError(ApplicationContext, e)
93
- }
94
- const name = `${storyId} ${tasksDir}`
95
- const { config } = projectConfig = await configureProject({projectPath: storyProjectPath, configPath, tasksDir, name})
96
- return new StoryProject(projectPath, config, storyId, ApplicationContext)
97
- }
98
-
99
- async function newConfigFromFile(configPath) {
100
- const config = await readFile(configPath, 'utf8')
101
- return new Config(loadYAML(config))
102
- }
103
-
104
- function logFsError(ApplicationContext, e) {
105
- ApplicationContext().log(`${e.code}: ${e.path}`)
106
- }
107
-
108
- module.exports = {
109
- projectExists,
110
- configureProject,
111
- newBacklogProject,
112
- newStoryProject,
113
- defaultProjectPath
114
- }
@@ -1,6 +0,0 @@
1
- module.exports = () => {
2
- return {
3
- log: require('./LogAdapter').log,
4
- StorageAdapter: require('./StorageAdapter'),
5
- }
6
- }
@@ -1,8 +0,0 @@
1
- module.exports = {
2
- ChangesExistError: class extends Error {
3
- constructor(action) {
4
- super(`There are changes on the current branch. Please commit or stash them before you ${action}.`)
5
- this.name = "ChangesExistError"
6
- }
7
- }
8
- }
@@ -1,3 +0,0 @@
1
- module.exports = {
2
- log: console.log,
3
- }