From 40f9f3aeedf98698948c60ffd2749e4d521bf0a5 Mon Sep 17 00:00:00 2001 From: Jesse Piascik Date: Fri, 13 Sep 2024 18:48:13 -0400 Subject: [PATCH 1/6] Remove Story Tasking Plugin and cli --- backlog/devex/remove-unused-code.md | 3 +- cli.js | 225 ------------ lib/cli/CliControler.js | 327 ------------------ lib/cli/MDStoryParser.js | 139 -------- lib/cli/ProjectConfigFactory.js | 114 ------ lib/cli/__tests__/CliController.spec.js | 106 ------ lib/cli/__tests__/MDStoryParser.spec.js | 122 ------- .../SetUpTestBacklogProjectWithRemote.js | 66 ---- lib/cli/adapters/ApplicationContext.js | 6 - lib/cli/adapters/Errors.js | 8 - lib/cli/adapters/LogAdapter.js | 3 - lib/cli/adapters/StorageAdapter.js | 135 -------- lib/cli/domain/BacklogProject.js | 170 --------- lib/cli/domain/StoryProject.js | 67 ---- lib/cli/prompts/MarkdownPlanPrompt.js | 16 - lib/plugins/plugin-manager.js | 2 +- 16 files changed, 3 insertions(+), 1506 deletions(-) delete mode 100755 cli.js delete mode 100644 lib/cli/CliControler.js delete mode 100644 lib/cli/MDStoryParser.js delete mode 100644 lib/cli/ProjectConfigFactory.js delete mode 100644 lib/cli/__tests__/CliController.spec.js delete mode 100644 lib/cli/__tests__/MDStoryParser.spec.js delete mode 100644 lib/cli/__tests__/SetUpTestBacklogProjectWithRemote.js delete mode 100644 lib/cli/adapters/ApplicationContext.js delete mode 100644 lib/cli/adapters/Errors.js delete mode 100644 lib/cli/adapters/LogAdapter.js delete mode 100644 lib/cli/adapters/StorageAdapter.js delete mode 100644 lib/cli/domain/BacklogProject.js delete mode 100644 lib/cli/domain/StoryProject.js delete mode 100644 lib/cli/prompts/MarkdownPlanPrompt.js diff --git a/backlog/devex/remove-unused-code.md b/backlog/devex/remove-unused-code.md index 6b5923b6..afc8b679 100644 --- a/backlog/devex/remove-unused-code.md +++ b/backlog/devex/remove-unused-code.md @@ -2,10 +2,11 @@ ## Tasks -- [ ] remove story tasking +- [x] remove story tasking - [ ] Remove line parsing \ No newline at end of file diff --git a/cli.js b/cli.js deleted file mode 100755 index 31731ed6..00000000 --- a/cli.js +++ /dev/null @@ -1,225 +0,0 @@ -#!/usr/bin/env node - -const { program } = require('commander'); -const ora = require('ora') -const chalk = require('chalk') -const { ChangesExistError } = require('./lib/cli/adapters/Errors') -const { - imdoneInit, - planStory, - startTask, - addTask, - listTasks , - completeTask, - openBoard, - openTaskFile, - exportProject -} = require('./lib/cli/CliControler') -const { - STORY_ID -} = require('./lib/cli/domain/BacklogProject').constants -const package = require('./package.json') - -const { log } = hideLogs() -const spinner = ora('Loading unicorns') - -setTimeout(() => { - spinner.color = 'yellow'; - spinner.text = 'Loading rainbows'; -}, 1000); - -function actionCancelled() { - log(chalk.bgRed('Action canceled')) -} - -const STORY_OPTION = `-s, --${STORY_ID} ` -const STORY_OPTION_OPTIONAL = `${STORY_OPTION}[${STORY_ID}]` -const STORY_OPTION_REQUIRED = `${STORY_OPTION}<${STORY_ID}>` - -const mainCmd = program -.version(package.version, '-v, --version', 'output the current version') - -mainCmd -.command('export') -.description('Export the current project') -.action(async function () { - try { - await exportProject(log) - } catch (e) { - console.error(e) - actionCancelled() - } finally { - process.exit(0) - } -}) - -// addDevCmd(mainCmd) - -program.parse(); - -function hideLogs() { - const log = console.log - const info = console.info - const warn = console.warn - const logQueue = {warn: [], info: [], log: []} - if (!process.env.DEBUG) { - Object.keys(logQueue).forEach((key) => { - console[key] = function(...args) { - logQueue[key].push(args) - } - }) - } - return {log, info, warn, logQueue} -} - - -async function planAStory(markdown, storyId, log) { - try { - await planStory(markdown, storyId, log) - } catch (e) { - log(chalk.yellowBright(e.message)) - } -} - -function addDevCmd(mainCmd) { - const devCmd = mainCmd - .command('dev') - .description('Plan and track your storys and tasks') - - devCmd - .command('init') - .description('initialize backlog') - .action(async function () { - try { - await imdoneInit() - } catch (e) { - log(chalk.yellowBright(e.message)) - } finally { - process.exit(0) - } - }) - - devCmd - .command('plan') - .option(STORY_OPTION_OPTIONAL, 'Update an existing story\'s tasks') - .description('Plan a story with tasks and DoD') - .action(async function () { - let markdown - let { storyId } = this.opts() - if (process.stdin.isTTY) { - await planAStory(markdown, storyId, log) - process.exit(0) - } else { - - const stdin = process.stdin; - spinner.start() - - markdown = '' - - stdin.on('readable', function() { - var chunk = stdin.read(); - if(chunk !== null){ - markdown += chunk; - } - }); - stdin.on('end', async function() { - await planAStory(markdown, storyId, log) - spinner.stop() - process.exit(0) - }); - } - }) - - devCmd - .command('open') - .description('Open the current or selected task in the default markdown editor') - .action(async function () { - try { - await openTaskFile(log) - } catch (e) { - log(chalk.yellowBright(e.message)) - } finally { - process.exit(0) - } - }) - - devCmd - .command('board') - .description('Open the current task in imdone') - .action(async function () { - try { - spinner.start() - await openBoard(log) - spinner.stop() - } catch (e) { - log(chalk.yellowBright(e.message)) - } finally { - process.exit(0) - } - }) - - devCmd - .command('start [task-id]') - .description('Start a task by id') - .action(async function () { - const taskId = this.args.length > 0 ? this.args[0]: null - try { - await startTask(taskId, log) - } catch (e) { - if (e instanceof ChangesExistError) { - log(chalk.yellowBright(e.message)) - } else { - actionCancelled() - } - } finally { - process.exit(0) - } - }) - - devCmd - .command('done') - .description('Mark the current task as done') - .action(async function () { - try { - await completeTask(log) - } catch (e) { - actionCancelled() - } finally { - process.exit(0) - } - }) - - devCmd - .command('add-task ') - .description('Add a task to a story') - .option(STORY_OPTION_REQUIRED, 'The story to add this task to') - .option('-g, --group ', 'The group to add this task to') - .action(async function () { - let { storyId, group } = this.opts() - try { - await addTask({content: this.args[0], storyId, group, log}) - } catch (e) { - actionCancelled() - } finally { - process.exit(0) - } - }) - - devCmd - .command('ls') - .description('list tasks') - .option(STORY_OPTION_REQUIRED, 'The story to list tasks for') - .option('-f, --filter ', 'The filter to use') - .option('-j, --json', 'Output as json') - .action(async function () { - let {storyId, filter, json } = this.opts() - try { - await listTasks({storyId, filter, json, log}) - } catch (e) { - - actionCancelled() - } finally { - process.exit(0) - } - }) -} diff --git a/lib/cli/CliControler.js b/lib/cli/CliControler.js deleted file mode 100644 index caf58e87..00000000 --- a/lib/cli/CliControler.js +++ /dev/null @@ -1,327 +0,0 @@ -const ApplicationContext = require('./adapters/ApplicationContext') -const {simpleGit} = require('simple-git') -const prompts = require('prompts') -const eol = require('eol') -const chalk = require('chalk') -const BacklogProject = require('./domain/BacklogProject') -const StoryProject = require('./domain/StoryProject') -const { createFileSystemProject } = require('../project-factory.js') -const commandExists = require('command-exists') -const git = simpleGit() -const _path = require('path') -const { - importMarkdown, - } = require('./MDStoryParser') - -let consoleLog = () => {} -const applicationContext = () => { - return { - ...ApplicationContext(), - log: consoleLog - } -} - -const { - configureProject, - defaultProjectPath, - projectExists, -} = require('./ProjectConfigFactory') - -const {promptForMarkdownPlan} = require('./prompts/MarkdownPlanPrompt.js') -const { - TASK_ID, - UNGROUPED_TASKS -} = BacklogProject.constants - -const replacer = (key, value) => - value instanceof Object && !(value instanceof Array) ? - Object.keys(value) - .sort() - .reduce((sorted, key) => { - sorted[key] = value[key]; - return sorted - }, {}) : - value; - -module.exports = CliController = {} - -CliController.exportProject = async function (log, verbose = false) { - const project = createFileSystemProject({path: process.env.PWD}) - await project.init() - let jsonOutput = verbose - ? project.toImdoneJSON() - : (() => { - const result = {} - const imdoneJSON = project.toImdoneJSON() - const lists = imdoneJSON.lists.map(list => { - list.tasks = list.tasks.map(task => { - const result = { - created: task.created, - completed: task.completed, - due: task.due, - path: _path.join(task.source.repoId, task.source.path), - meta: task.allMeta, - tags: task.allTags, - context: task.allContext, - line: task.line, - content: task.content, - interpretedContent: task.interpretedContent, - lastLine: task.lastLine, - list: task.list, - progress: task.progress, - order: task.order, - beforeText: task.beforeText, - props: task.props, - } - return result - }) - return list - }) - result.lists = lists - result.totals = imdoneJSON.totals - result.tags = imdoneJSON.tags - - return result - })() - - log(JSON.stringify(jsonOutput, replacer, 3)) -} - -CliController.imdoneInit = async function () { - if (await projectExists(defaultProjectPath())) { - throw new Error('Imdone backlog is already initialized!') - } - const defaultBranch = await promptForDefaultBranch() - const remote = await promptForRemote() - await configureProject({defaultBranch, remote}) -} - -CliController.openBoard = async function (log) { - const open = (await import('open')).default - try { - await commandExists('imdone') - const { storyProject, task } = await getCurrentTask() - const taskId = task && task.meta[TASK_ID] && task.meta[TASK_ID][0] - - if (storyProject) { - const queryString = taskId ? `?filter=meta.${TASK_ID}="${taskId}"` : '' - open(`imdone://${storyProject.path}${queryString}`) - } else { - open(`imdone://${defaultProjectPath()}`) - } - } catch (e) { - console.error(e) - log(`\nimdone is not installed! Visit ${chalk.green('https://imdone.io')} to install imdone.`) - open(`https://imdone.io`) - } -} - -CliController.openTaskFile = async function (log) { - const open = (await import('open')).default - let { task } = await getCurrentTask() - if (!task) { - const storyId = await promptForStoryId() - const taskId = await promptForTaskId(storyId) - const storyProject = await StoryProject.createAndInit(defaultProjectPath(), null, storyId, applicationContext) - task = storyProject.getTask(taskId) || storyProject.getStory() - } - - open(task.fullPath) -} - - -// TODO: This should ask for a group to add a task to -CliController.addTask = async function ({content, storyId, group, log}) { - consoleLog = log - storyId = await determineStoryId(storyId) - const storyProject = await StoryProject.createAndInit(defaultProjectPath(), null, storyId, applicationContext) - - const task = await storyProject.addTask({group, content}) - - log(`${TASK_ID}:"${getTaskId(task)}"`) -} - -CliController.listTasks = async function ({storyId, filter=null, json, log}) { - consoleLog = log - storyId = storyId || await determineStoryId(storyId) - - const storyProject = await StoryProject.createAndInit(defaultProjectPath(), null, storyId, applicationContext) - - const tasks = storyProject.getTasks(filter) - - if (json) return log( - JSON.stringify({ - name: storyProject.name, - path: storyProject.path, - tasks: storyProject.tasksToJson(tasks) - }, null, 2) - ) - - const story = storyProject.getStory() - const groupedTasks = getGroupedTasks(tasks) - logStory(log, storyProject, story) - logGroupedTasks(log, storyProject, groupedTasks) -} - -CliController.planStory = async function(markdown, storyId, log = console.log) { - consoleLog = log - if (storyId) { - storyId = await determineStoryId(storyId) - } - if (!markdown) { - markdown = await promptForMarkdownPlan() - } - return await importMarkdown({ - projectPath: defaultProjectPath(), - existingStoryId: storyId, - markdown, - applicationContext - }) -} - -CliController.startTask = async function (taskId, log) { - consoleLog = log - if (!taskId) { - const storyId = await determineStoryId() - taskId = await promptForTaskId(storyId) - } - - if (!taskId) return log(chalk.bgRed('No tasks found!')) - const project = await BacklogProject.createAndInit(defaultProjectPath(), null, applicationContext) - await project.startTask(taskId) -} - -CliController.completeTask = async function (log) { - consoleLog = log - const {storyProject, task} = await getCurrentTask() - if (!task) return noTaskStarted(log) - try { - const task = await storyProject.completeTask() - log() - logTask(log, storyProject, task) - log(chalk.bgGreen('Remember to commit and push!')) - } catch (e) { - log(chalk.bgRed(e.message)) - } -} - -function noTaskStarted(log) { - log('\n', chalk.bgRed('No task started!')) -} -function getTaskId(task) { - const taskIdMeta = task.meta[TASK_ID] - return taskIdMeta ? taskIdMeta[0] : null -} - -async function getCurrentTask() { - const backlogProject = await BacklogProject.createAndInit(defaultProjectPath(), null, applicationContext) - const storyId = backlogProject.getCurrentStoryId() - if (!storyId) return { backlogProject } - const storyProject = await StoryProject.createAndInit(defaultProjectPath(), null, storyId, applicationContext) - const task = await storyProject.getCurrentTask() - return { backlogProject, storyProject, storyId, task } -} - -async function determineStoryId(storyId) { - const project = await BacklogProject.createAndInit(defaultProjectPath(), null, applicationContext) - storyId = storyId || project.getCurrentStoryId() - storyId = await promptForStoryId(storyId) - return storyId -} - -async function promptForStoryId(lastStoryId) { - const backlogProject = new BacklogProject(defaultProjectPath(), null, applicationContext) - await backlogProject.init() - - const storyIds = backlogProject.getStoryIds().filter(({list}) => list !== backlogProject.config.getDoneList()) - const choices = storyIds.map(({storyId, text}) => ({ title: `${text} [ ${storyId} ]`, value: storyId })) - let initial = storyIds.findIndex(({storyId}) => storyId === lastStoryId) - if (initial < 0) initial = 0 - const result = await prompts({ - type: 'select', - name: 'storyId', - message: 'Select a story', - choices, - initial - }) - return result.storyId -} - -async function promptForTaskId(storyId) { - const storyProject = await StoryProject.createAndInit(defaultProjectPath(), null, storyId, applicationContext) - const tasks = storyProject.getTasks(`list != ${storyProject.config.getDoneList()}`) - const choices = tasks.map(({text, meta}) => ({ title: `${text} [ ${meta[TASK_ID]} ]`, value: meta[TASK_ID] })) - const result = await prompts({ - type: 'select', - name: 'taskId', - message: 'Select a task', - choices - }) - return result.taskId -} - -async function promptForDefaultBranch() { - const branches = (await git.branchLocal()).all - const initial = branches.includes('master') ? 'master' : 'main' - const result = await prompts({ - type: 'text', - name: 'defaultBranch', - message: 'Enter the name of the default branch', - initial - }) - return result.defaultBranch -} - -async function promptForRemote() { - const remotes = (await git.getRemotes(true)).map(({name}) => ({title: name, value: name})) - if (remotes.length === 1) return remotes[0].value - if (remotes.length === 0) return - - const result = await prompts({ - type: 'select', - name: 'remote', - message: 'Select a remote', - choices: remotes - }) - return result.remote -} - -function getGroupedTasks(tasks) { - const groupedTasks = {} - tasks.forEach((task) => { - const group = task.meta.group - if (group) { - if (!groupedTasks[group]) groupedTasks[group] = [] - groupedTasks[group].push(task) - } - }) - return groupedTasks -} - -function logGroupedTasks(log, project, groupedTasks) { - log('\n## Tasks\n') - Object.keys(groupedTasks).forEach((group) => { - if (group !== UNGROUPED_TASKS) { - log('') - log(`### ${group}`) - log('') - } - logTasks(log, project, groupedTasks[group]) - }) -} - -function logTasks(log, project, tasks) { - tasks.forEach(task => logTask(log, project, task)) -} - -function logStory(log, project, story) { - log(`# ${project.name}`) - log('') - log(`${story.text}`) - log(story.description.filter(ln => !/^$/.test(ln)).join(String(eol.lf))) -} - -function logTask(log, project, task) { - const doneMark = (task.list === project.config.getDoneList()) ? 'x' : ' ' - log(`- [${doneMark}] ${task.text} `) -} diff --git a/lib/cli/MDStoryParser.js b/lib/cli/MDStoryParser.js deleted file mode 100644 index b5f6f237..00000000 --- a/lib/cli/MDStoryParser.js +++ /dev/null @@ -1,139 +0,0 @@ -const { parseMD } = require('../tools') -const StoryProject = require('./domain/StoryProject') -const BacklogProject = require('./domain/BacklogProject') -const { - UNGROUPED_TASKS, - TODO, - DONE, - ORDER -} = BacklogProject.constants -const ApplicationContext = require('./adapters/ApplicationContext') - -function isOpen(token, type, tag = token.tag) { - return token.type === type + '_open' && token.tag === tag -} - -function isClose(token, type, tag = token.tag) { - return token.type === type + '_close' && token.tag === tag -} - -function getHeadingLevel(tag) { - return parseInt(tag.substring(1), 10) -} - -function parse(markdown) { - const heading = 'heading' - const paragraph = 'paragraph' - const inline = 'inline' - const bulletList = 'bullet_list' - const listItem = 'list_item' - const textType = 'text' - let storyText, description = [], groupName = '', tasks = [] - let inHeading = false - let inParagraph = false - let inBulletList = false - let inListItem = false - let inTasks = false - let headingLevel = 0 - const ast = parseMD(markdown) - - ast.forEach((token) => { - const { type, tag, markup, content, info, children } = token - if (isOpen(token, heading)) { - inHeading = true - headingLevel = getHeadingLevel(tag) - } else if (isClose(token, heading)) { - inHeading = false - } - - if (isOpen(token, bulletList)) { - inBulletList = true - } else if (isClose(token, bulletList)) { - inBulletList = false - } - - if (isOpen(token, listItem)) { - inListItem = true - } else if (isClose(token, listItem)) { - inListItem = false - } - - if (isOpen(token, paragraph)) { - inParagraph = true - } else if (isClose(token, paragraph)) { - inParagraph = false - } - - if (inHeading && type == inline) { - groupName = content - if (headingLevel == 1) { - storyText = groupName - } - } - - if (groupName.toLowerCase().endsWith('tasks') && headingLevel == 2) { - inTasks = true - } - - if (inTasks && inBulletList && inListItem && inParagraph && type == inline) { - const group = headingLevel > 2 ? groupName : UNGROUPED_TASKS - const done = /^\[x\]\s/.test(content) - const description = children - .filter(child => child.type === textType) - .map(child => child.content) - const text = description.join('\n') - tasks.push({ group, text, done }) - } - }) - - if (storyText) { - const result = new RegExp(`# ${storyText}\n(.*)## Tasks`, 'gms').exec(markdown) - description = result && result.length > 1 ? result[1].trim() : '' - } - - return { storyText, description, tasks } -} - -async function importMarkdown({projectPath, existingStoryId, markdown, applicationContext = ApplicationContext}) { - const { log } = applicationContext() - - const backlogProject = await BacklogProject.createAndInit(projectPath, null, applicationContext) - - let { storyText, description, tasks } = parse(markdown) - - const storyId = existingStoryId || backlogProject.toStoryId(storyText) - - log(`Importing story ${storyId}\n${description}`) - - await backlogProject.storageAdapter.initStoryPlan(storyId) - - if (!existingStoryId) await backlogProject.addStory(`${storyText}\n${description}`) - - const storyProject = await StoryProject.createAndInit(projectPath, null, storyId, applicationContext) - - await addStoryTasks(storyProject, tasks) - - await storyProject.init() - - await backlogProject.storageAdapter.saveStoryPlan(storyId) - - return storyProject -} - -async function addStoryTasks(storyProject, tasks) { - tasks.forEach(async (task, i) => { - const order = (i + 1) * (10) - const list = task.done ? DONE : TODO - const meta = [ - { key: 'group', value: task.group }, - { key: ORDER, value: order } - ] - await storyProject.addTaskToFile({list, content: task.text, meta}) - }) -} - -module.exports = { - UNGROUPED_TASKS, - parse, - importMarkdown -} diff --git a/lib/cli/ProjectConfigFactory.js b/lib/cli/ProjectConfigFactory.js deleted file mode 100644 index 95946fea..00000000 --- a/lib/cli/ProjectConfigFactory.js +++ /dev/null @@ -1,114 +0,0 @@ -const DefaultApplicationContext = require('./adapters/ApplicationContext') -const { loadYAML, dumpYAML } = require('../adapters/yaml') -const Config = require('../config') -const { readFile, mkdir, cp, rm, writeFile, access } = require('fs').promises -const { resolve } = path = require('path') -const BacklogProject = require('./domain/BacklogProject') -const StoryProject = require('./domain/StoryProject') - -const DEFAULT_PROJECT_DIR = 'backlog' -const STORIES_DIR = 'stories' -const TASKS_DIR = 'tasks' -const CONFIG_PATH = path.join(__dirname, '..', '..', 'devops', 'imdone') -const BACKLOG_CONFIG_PATH = path.join(CONFIG_PATH, 'backlog-config.yml') -const STORY_CONFIG_PATH = path.join(CONFIG_PATH, 'story-config.yml') -const BACKLOG_IGNORE_PATH = path.join(CONFIG_PATH, 'backlog.imdoneignore') - -const defaultProjectPath = (baseDir = process.env.PWD) => path.join(baseDir, DEFAULT_PROJECT_DIR) - -const getProjectConfigPath = (projectPath = defaultProjectPath()) => path.join(projectPath, '.imdone', 'config.yml') - -const projectExists = async ( - projectPath = defaultProjectPath() -) => { - const projectConfigPath = getProjectConfigPath(projectPath) - try { - await access(projectConfigPath) - return projectConfigPath - } catch (e) { - return false - } -} - -const configureProject = async ({ - projectPath = defaultProjectPath(), - configPath = BACKLOG_CONFIG_PATH, - tasksDir = STORIES_DIR, - defaultBranch, - remote, - name -}) => { - projectPath = resolve(projectPath) - configPath = resolve(configPath) - - if (await projectExists(projectPath)) { - return { projectPath, config: await newConfigFromFile(projectConfigPath)} - } - - const projectConfigPath = getProjectConfigPath(projectPath) - - await mkdir(path.join(projectPath, tasksDir), {recursive: true}) - await mkdir(path.join(projectPath, '.imdone'), { recursive: true }) - - const config = await newConfigFromFile(configPath) - const parentDir = path.basename(path.dirname(projectPath)) - config.name = name || `${parentDir} ${DEFAULT_PROJECT_DIR}` - config.journalPath = tasksDir - const pluginConfig = config.settings.plugins.CliBacklogPlugin - config.settings.plugins.CliBacklogPlugin = {...pluginConfig, defaultBranch, remote} - - await writeFile(projectConfigPath, dumpYAML(config)) - return { projectPath, config } -} - -const newBacklogProject = async ({ - projectPath = defaultProjectPath(), - configPath = BACKLOG_CONFIG_PATH, - tasksDir = STORIES_DIR, - ApplicationContext = DefaultApplicationContext -}) => { - try { - await cp(BACKLOG_IGNORE_PATH, path.join(projectPath, '.imdoneignore')) - } catch (e) { - logFsError(ApplicationContext, e) - } - - const { config } = projectConfig = await configureProject({projectPath, configPath, tasksDir}) - return new BacklogProject(projectConfig.projectPath, config, ApplicationContext) -} - -const newStoryProject = async ({ - projectPath = defaultProjectPath(), - configPath = STORY_CONFIG_PATH, - tasksDir = TASKS_DIR, - storyId, - ApplicationContext = DefaultApplicationContext -}) => { - const storyProjectPath = BacklogProject.getStoryProjectPath(projectPath, storyId) - try { - await rm(storyProjectPath, { recursive: true }) - await mkdir(storyProjectPath, { recursive: true }) - } catch (e) { - logFsError(ApplicationContext, e) - } - const name = `${storyId} ${tasksDir}` - const { config } = projectConfig = await configureProject({projectPath: storyProjectPath, configPath, tasksDir, name}) - return new StoryProject(projectPath, config, storyId, ApplicationContext) -} - -async function newConfigFromFile(configPath) { - const config = await readFile(configPath, 'utf8') - return new Config(loadYAML(config)) -} - -function logFsError(ApplicationContext, e) { - ApplicationContext().log(`${e.code}: ${e.path}`) -} - -module.exports = { - projectExists, - configureProject, - newBacklogProject, - newStoryProject, - defaultProjectPath -} \ No newline at end of file diff --git a/lib/cli/__tests__/CliController.spec.js b/lib/cli/__tests__/CliController.spec.js deleted file mode 100644 index d50282d4..00000000 --- a/lib/cli/__tests__/CliController.spec.js +++ /dev/null @@ -1,106 +0,0 @@ -const { - getBeforeEach, - contextualDescribe -} = require('./SetUpTestBacklogProjectWithRemote') -const BacklogProject = require("../domain/BacklogProject") -const { - planStory, - addTask, - startTask -} = require('../CliControler') -const markdown = `# my story id - -This is the story description. - -- [ ] A simple task - -\`\`\`md -This is a block of markdown -so it can contain anything -- [ ] A task -- [x] Another task -\`\`\` - -And a paragraph after the code block. - -## Tasks -- [ ] An unfinished task - -### Phase one (Interfaces) -- [x] A task in phase one -- [ ] Another task in phase one - Some more info about the task - - [ ] A sub task in phase one - Some more data about the task - -### Phase two (Implementation) -- [ ] A task in phase two -` -contextualDescribe('CliController', async () => { - let { backlogProject, git, beforeEachFunc } = getBeforeEach() - beforeEach(async () => { - const result = await beforeEachFunc() - backlogProject = result.backlogProject - git = result.git - }) - - describe('planStory', async () => { - it('should import a story from markdown', async () => { - const storyId = 'my-story-id' - const storyProject = await planStory(markdown) - const branch = (await git.branchLocal()).current - - should(storyProject.storyId).equal(storyId) - - const tasks = storyProject.getTasks() - should(tasks.length).equal(5) - should(tasks[0].text).equal('An unfinished task') - should(tasks[1].text).equal('A task in phase one') - should(tasks[2].text).equal('Another task in phase one') - should(tasks[3].text).equal('A sub task in phase one') - should(tasks[4].text).equal('A task in phase two') - should(branch).equal(`story/${storyId}/main`) - should((await git.status()).isClean()).be.true() - }) - }) - - describe('startTask', async () => { - it('should move the task and story to DOING and create a task branch', async () => { - const storyId = 'my-story-id' - const storyProject = await planStory(markdown) - const tasks = storyProject.getTasks() - const task = tasks[0] - - const taskId = storyProject.getTaskId(task) - const taskName = backlogProject.getTaskName(task) - await startTask(taskId, console.log) - - const currentBranch = (await git.branchLocal()).current - should(currentBranch).be.equal(`story/${storyId}/task/${taskName}`) - await storyProject.init() - const currentTask = await storyProject.getCurrentTask() - currentTask.list.should.equal(BacklogProject.constants.DOING) - storyProject.getStory().list.should.equal(BacklogProject.constants.DOING) - }) - }) - - describe.skip('addTask', async () => { - it('should add a task to the story', async () => { - const storyId = 'my-story-id' - const storyProject = await planStory(markdown) - const tasks = storyProject.getTasks() - const task = tasks[0] - - const taskId = storyProject.getTaskId(task) - const taskName = backlogProject.getTaskName(task) - await backlogProject.startTask(taskId) - - const currentBranch = (await git.branchLocal()).current - should(currentBranch).be.equal(`story/${storyId}/task/${taskName}`) - await storyProject.init() - const currentTask = await storyProject.getCurrentTask() - currentTask.list.should.equal(BacklogProject.constants.DOING) - storyProject.getStory().list.should.equal(BacklogProject.constants.DOING) - }) - }) -}) diff --git a/lib/cli/__tests__/MDStoryParser.spec.js b/lib/cli/__tests__/MDStoryParser.spec.js deleted file mode 100644 index 8f7620d7..00000000 --- a/lib/cli/__tests__/MDStoryParser.spec.js +++ /dev/null @@ -1,122 +0,0 @@ -const { - getBeforeEach, - contextualDescribe -} = require('./SetUpTestBacklogProjectWithRemote') -const { parse, importMarkdown } = require("../MDStoryParser") -const expect = require("chai").expect - -const MARKDOWN = `# story-id - -This is the story description. - -- [ ] A simple task - -\`\`\`md -This is a block of markdown -so it can contain anything -- [ ] A task -- [x] Another task -\`\`\` - -And a paragraph after the code block. - -## Tasks -- [ ] An unfinished task - -### Phase one (Interfaces) -- [x] A task in phase one -- [ ] Another task in phase one - Some more data about the task! - - [ ] A sub task in phase one - Some more data about the task - -### Phase two (Implementation) -- [ ] A task in phase two -` - -TASKS_MARKDOWN = ` -## Tasks - -- [ ] A new imported task A -- [ ] A new imported task B -` -const DESCRIPTION = MARKDOWN.split('\n').slice(2,14).join('\n') - -describe('MDStoryParser.parse', () => { - it('should parse a markdown story name', () => { - const { storyText, description, tasks } = parse(MARKDOWN) - expect(storyText).to.equal('story-id') - expect(description).to.equal(DESCRIPTION) - expect(tasks.length).to.equal(5) - expect(tasks[0].text).to.equal('An unfinished task') - expect(tasks[0].group).to.equal('Ungrouped Tasks') - expect(tasks[0].done).to.equal(false) - expect(tasks[1].text).to.equal('A task in phase one') - expect(tasks[1].group).to.equal('Phase one (Interfaces)') - expect(tasks[1].done).to.equal(true) - expect(tasks[2].text).to.equal('Another task in phase one\nSome more data about the task!') - expect(tasks[2].group).to.equal('Phase one (Interfaces)') - expect(tasks[2].done).to.equal(false) - expect(tasks[3].text).to.equal("A sub task in phase one\nSome more data about the task") - expect(tasks[3].group).to.equal('Phase one (Interfaces)') - expect(tasks[3].done).to.equal(false) - expect(tasks[4].text).to.equal('A task in phase two') - expect(tasks[4].group).to.equal('Phase two (Implementation)') - expect(tasks[4].done).to.equal(false) - }) -}) - -contextualDescribe('MDStoryParser.importMarkdown', () => { - let { backlogProject, git, beforeEachFunc } = getBeforeEach() - beforeEach(async () => { - const result = await beforeEachFunc() - backlogProject = result.backlogProject - git = result.git - }) - - it('should import a story definition and tasks from markdown and return the storyProject', async () => { - const storyId = 'story-id' - const storyProject = await importMarkdown({projectPath: backlogProject.projectPath, markdown: MARKDOWN}) - const branch = (await git.branchLocal()).current - - should(storyProject.storyId).equal(storyId) - - const tasks = storyProject.getTasks() - should(tasks.length).equal(5) - should(tasks[0].text).equal('An unfinished task') - should(tasks[1].text).equal('A task in phase one') - should(tasks[2].text).equal('Another task in phase one') - should(tasks[3].text).equal('A sub task in phase one') - should(tasks[4].text).equal('A task in phase two') - should(branch).equal(`story/${storyId}/main`) - should((await git.status()).isClean()).be.true() - }) - - it('should import tasks from markdown to an existing story and return the storyProject', async () => { - const storyId = existingStoryId = 'story-id' - let storyProject = await importMarkdown({projectPath: backlogProject.projectPath, markdown: MARKDOWN}) - let branch = (await git.branchLocal()).current - - should(storyProject.storyId).equal(storyId) - - let tasks = storyProject.getTasks() - should(tasks.length).equal(5) - should(tasks[0].text).equal('An unfinished task') - should(tasks[1].text).equal('A task in phase one') - should(tasks[2].text).equal('Another task in phase one') - should(tasks[3].text).equal('A sub task in phase one') - should(tasks[4].text).equal('A task in phase two') - should(branch).equal(`story/${storyId}/main`) - should((await git.status()).isClean()).be.true() - - storyProject = await importMarkdown({projectPath: backlogProject.projectPath, markdown: TASKS_MARKDOWN, existingStoryId}) - branch = (await git.branchLocal()).current - - should(storyProject.storyId).equal(storyId) - - tasks = storyProject.getTasks() - should(tasks.length).equal(7) - - }) - -}) \ No newline at end of file diff --git a/lib/cli/__tests__/SetUpTestBacklogProjectWithRemote.js b/lib/cli/__tests__/SetUpTestBacklogProjectWithRemote.js deleted file mode 100644 index d26b1418..00000000 --- a/lib/cli/__tests__/SetUpTestBacklogProjectWithRemote.js +++ /dev/null @@ -1,66 +0,0 @@ -const isRunningInGithubAction = !!process.env.GITHUB_ACTION -const contextualDescribe = describe.skip // Skipped until we replace with generic task creation functions -// const contextualDescribe = isRunningInGithubAction ? describe.skip : describe -const { configureProject, defaultProjectPath } = require("../ProjectConfigFactory") -const BacklogProject = require("../domain/BacklogProject") -const fs = require('fs').promises -const path = require('path') -const projectRoot = path.dirname(require.resolve('../../../package.json')) -const simpleGit = require('simple-git') - -const GIT_SSH_COMMAND = "ssh -i ~/.ssh/id_rsa -o StrictHostKeyChecking=no" -const ORIGIN = 'ssh://git@localhost:8022/home/git/test-project.git' -const projectPath = path.join(projectRoot, 'temp', 'test-project') -const defaultBranch = 'main' -const remote = 'origin' -const backlogProjectPath = defaultProjectPath(projectPath) -let backlogProject -let git - -function execCmd(cmd) { - console.log('executing:', cmd) - const exec = require('child_process').exec; - return new Promise((resolve, reject) => { - exec(cmd, (error, stdout, stderr) => { - if (error) { - console.warn(error); - return reject(error) - } - console.log(`stdout: ${stdout} stderr: ${stderr}`); - resolve(stdout? stdout : stderr); - }); - }); -} - -function getBeforeEach() { - return { - beforeEachFunc: async () => { - await execCmd(path.join(projectRoot, 'devops', 'containers', 'setup-remote.sh')) - try { - await fs.rmdir(projectPath, {recursive: true}) - } catch (e) { - console.log('no temp project to remove') - } finally { - await fs.mkdir(projectPath, {recursive: true}) - } - process.env.PWD = projectPath - git = simpleGit(projectPath).env({...process.env, GIT_SSH_COMMAND}) - const result = await configureProject({ - projectPath: backlogProjectPath, - defaultBranch, - remote - }) - const { config } = result - backlogProject = await BacklogProject.createAndInit(backlogProjectPath, config) - await git.init().add('.').commit('initial commit') - await git.addRemote(remote, ORIGIN) - await git.push(remote, defaultBranch, {'--set-upstream': null}) - return {backlogProject, git} - } - } -} - -module.exports = { - getBeforeEach, - contextualDescribe -} \ No newline at end of file diff --git a/lib/cli/adapters/ApplicationContext.js b/lib/cli/adapters/ApplicationContext.js deleted file mode 100644 index b7d610e3..00000000 --- a/lib/cli/adapters/ApplicationContext.js +++ /dev/null @@ -1,6 +0,0 @@ -module.exports = () => { - return { - log: require('./LogAdapter').log, - StorageAdapter: require('./StorageAdapter'), - } -} \ No newline at end of file diff --git a/lib/cli/adapters/Errors.js b/lib/cli/adapters/Errors.js deleted file mode 100644 index c1393da7..00000000 --- a/lib/cli/adapters/Errors.js +++ /dev/null @@ -1,8 +0,0 @@ -module.exports = { - ChangesExistError: class extends Error { - constructor(action) { - super(`There are changes on the current branch. Please commit or stash them before you ${action}.`) - this.name = "ChangesExistError" - } - } -} \ No newline at end of file diff --git a/lib/cli/adapters/LogAdapter.js b/lib/cli/adapters/LogAdapter.js deleted file mode 100644 index 29605f7f..00000000 --- a/lib/cli/adapters/LogAdapter.js +++ /dev/null @@ -1,3 +0,0 @@ -module.exports = { - log: console.log, -} \ No newline at end of file diff --git a/lib/cli/adapters/StorageAdapter.js b/lib/cli/adapters/StorageAdapter.js deleted file mode 100644 index 5e099995..00000000 --- a/lib/cli/adapters/StorageAdapter.js +++ /dev/null @@ -1,135 +0,0 @@ -const simpleGit = require('simple-git') -const {changesExistError, ChangesExistError} = require('./Errors') - -module.exports = class { - - constructor(project) { - this.project = project - this.git = simpleGit(this.path) - } - - get path() { - return this.project.path - } - - get remote() { - return this.pluginConfig.remote - } - - get defaultBranch() { - return this.pluginConfig.defaultBranch - } - - get pluginConfig() { - return this.project.config.settings.plugins.CliBacklogPlugin - } - - async initTaskUpdate(task) { - if (await this.changesExist()) { - throw new ChangesExistError('start a task') - } - - await this.checkoutStoryBranch({task}) - } - - async commitTaskUpdate(task) { - const taskBranchName = this.getTaskBranchName(task) - await this.project.addMetadata(task, 'branch', taskBranchName) - await this.git.add('.').commit(`Update task ${this.getTaskName(task)}`) - await this.git.push(this.remote) - await this.git.checkoutBranch(taskBranchName, this.getStoryBranchName({task})) - } - - async initStoryPlan(storyId) { - if (await this.changesExist()) { - throw new ChangesExistError('plan a story') - } - - await this.checkoutStoryBranch({storyId}) - } - - async saveStoryPlan(storyId) { - const storyBranchName = this.getStoryBranchName({storyId}) - await this.git.add('.').commit(`Update story ${storyId}`) - await this.git.push(this.remote) - } - - getTaskName(task) { - return this.project.getTaskName(task) - } - - getStoryId(task) { - return this.project.getStoryId(task) - } - - getStoryBranchName({task, storyId}) { - return `${this.getStoryBranchPrefix({task, storyId})}/main` - } - - getStoryBranchPrefix({task, storyId}) { - return `story/${storyId || this.getStoryId(task)}` - } - - getTaskBranchName(task) { - const storyBranchPrefix = this.getStoryBranchPrefix({task}) - const taskName = this.getTaskName(task) - return `${storyBranchPrefix}/task/${taskName}` - } - - async getCurrentTask() { - const currentBranch = await this.getCurrentBranch() - const tasks = this.project.getTasks(`meta.branch="${currentBranch}"`) - return tasks && tasks.length == 1 && tasks[0] - } - - async getCurrentBranch() { - return (await this.git.branchLocal()).current - } - - async changesExist() { - return !(await this.git.status()).isClean() - } - - async checkoutDefaultBranch() { - const defaultBranch = this.defaultBranch - await this.fetch() - await this.git.checkout(defaultBranch) - await this.pull() - } - - async checkoutStoryBranch({task, storyId}) { - const storyBranchName = this.getStoryBranchName({task, storyId}) - - await this.checkoutDefaultBranch() - - if (await this.branchExists(storyBranchName)) { - await this.git.checkout(storyBranchName) - await this.pull() - } else { - await this.git.checkoutBranch( - storyBranchName, - this.defaultBranch - ) - await this.pushNewBranch(storyBranchName) - } - await this.git.merge([this.defaultBranch]) - } - - async branchExists(branchName) { - const branches = await this.git.branchLocal() - return branches.all.includes(branchName) - } - - async pull() { - await this.git.pull(this.remote) - } - - async fetch() { - await this.git.fetch(this.remote) - } - - async pushNewBranch(branchName) { - await this.git.push(this.remote, branchName, {'--set-upstream': null}) - } - -} diff --git a/lib/cli/domain/BacklogProject.js b/lib/cli/domain/BacklogProject.js deleted file mode 100644 index 5a647ce5..00000000 --- a/lib/cli/domain/BacklogProject.js +++ /dev/null @@ -1,170 +0,0 @@ -const { createFileSystemProject } = require('../../project-factory') -const { resolve } = path = require('path') -const STORIES_DIR = 'stories' -const STORY_ID = 'story-id' -const TASK_ID = 'task-id' -const TODO = 'TODO' -const DOING = 'DOING' -const DONE = 'DONE' -const ORDER = 'order' -const UNGROUPED_TASKS = 'Ungrouped Tasks' - -class BacklogProject { - constructor(projectPath, config, ApplicationContext = require('../adapters/ApplicationContext')) { - this.ApplicationContext = ApplicationContext - this.StorageAdapter = ApplicationContext().StorageAdapter - this.projectPath = resolve(projectPath) - this._config = config - this.project = null - } - - get path() { - return this.projectPath - } - - get name() { - return this.project.name - } - - get defaultList() { - return this.config.getDefaultList() - } - - get config() { - return this.project && this.project.config || this._config - } - - async init() { - this.project = createFileSystemProject({path: this.projectPath, config: this.config}) - await this.project.init() - this.storageAdapter = new this.StorageAdapter(this) - return this - } - - getStoryProjectPath(storyId) { - return getStoryProjectPath(this.path, storyId) - } - - async addStory(content) { - await this.addTaskToFile({list: TODO, content }) - } - - toStoryId(text) { - return this.project.sanitizeFileName(text) - } - - removeList(list) { - return this.project.removeList(list) - } - - getStoryIds() { - return [ - ...new Set( - this.project.getCards(`meta.${STORY_ID}=* AND tags=story`).map(({meta, text, list}) => ({text, list, storyId: meta[STORY_ID][0]})) - ) - ] - } - - tasksToJson(tasks) { - return tasks.map(({text, type, list, meta, content, tags, context, description, source, beforeText, order}) => - ({ - list, - type, - order, - path: source.path, - prefix: beforeText, - text, - content, - description, - tags, - context, - meta - }) - ) - } - - getTaskName(task) { - return this.toStoryId(task.text) - } - - async startTask(taskId) { - await this.init() - - const taskFilter = `meta.${TASK_ID}="${taskId}"` - const task = this.project.getAllCards(taskFilter)[0] - if (!task) throw new Error(`No task found with id ${taskId}`) - - const storyId = task.meta[STORY_ID][0] - const storyFilter = `tags=story AND meta.${STORY_ID}="${storyId}"` - const story = this.project.getAllCards(storyFilter)[0] - - await this.storageAdapter.initTaskUpdate(task) - await this.moveTask(task, DOING, 0) - await this.project.addMetadata(task, 'branch', this.storageAdapter.getTaskBranchName(task)) - this.project.rollBackFileForTask(task) - await this.moveTask(story, DOING, 0) - await this.storageAdapter.commitTaskUpdate(task) - } - - async saveStory(storyId) { - // // Switch to the story branch or create it - // await git.checkout(storyBranchName); - // await git.pull('origin', storyBranchName, { '--force': null }); - - // // Push the changes to the story branch with force - // await git.push('origin', storyBranchName, { '--force': null }); - - // // Return to the original branch - // await git.checkout(currentBranch); - - // Merge the story branch into the current branch - - } - - async moveTask(task, list, order) { - return await this.project.moveTask(task, list, order) - } - - async addTaskToFile({path, list, tags, content, meta, contexts=[]}) { - return await this.project.addTaskToFile({path, list, tags, content, meta, contexts, useCardTemplate: true}) - } - - async addMetadata(task, key, value) { - return await this.project.addMetadata(task, key, value) - } - - getCurrentStoryId() { - const tasks = this.project.getAllCards(`tags = "story" AND list = "${DOING}"`) - if (tasks.length < 1) return - return this.getStoryId(tasks[0]) - } - - getStoryId(task) { - return task.meta[STORY_ID][0] - } - - async getCurrentTask() { - return await this.storageAdapter.getCurrentTask() - } -} - -function getStoryProjectPath(projectPath, storyId) { - return path.join(projectPath, STORIES_DIR, storyId) -} - -BacklogProject.getStoryProjectPath = getStoryProjectPath -BacklogProject.createAndInit = async function(projectPath, config, ApplicationContext) { - return (await (new BacklogProject(projectPath, config, ApplicationContext)).init()) -} - -BacklogProject.constants = { - STORIES_DIR, - STORY_ID, - TASK_ID, - TODO, - DOING, - DONE, - ORDER, - UNGROUPED_TASKS -} -module.exports = BacklogProject \ No newline at end of file diff --git a/lib/cli/domain/StoryProject.js b/lib/cli/domain/StoryProject.js deleted file mode 100644 index 8be81ea6..00000000 --- a/lib/cli/domain/StoryProject.js +++ /dev/null @@ -1,67 +0,0 @@ -const path = require('path') -const BacklogProject = require('./BacklogProject') -const { - STORY_ID, - TASK_ID, - DONE, - DOING, - UNGROUPED_TASKS -} = BacklogProject.constants - -class StoryProject extends BacklogProject { - constructor(projectPath, config, storyId, ApplicationContext = require('../adapters/ApplicationContext')) { - super(BacklogProject.getStoryProjectPath(projectPath, storyId), config, ApplicationContext) - this.storyId = storyId - } - - async addTask({content, group = UNGROUPED_TASKS}) { - const file = await this.addTaskToFile({list: this.defaultList, content}) - file.rollback().extractTasks(this.config) - const task = file.tasks[0] - if (group !== UNGROUPED_TASKS) { - await this.project.removeMetadata(task, 'group', UNGROUPED_TASKS) - await this.project.addMetadata(task, 'group', group) - } - return file.tasks[0] - } - - async completeTask() { - const task = await this.getCurrentTask() - if (!task) throw new Error('No current task') - await this.project.moveTask(task, DONE) - return task - } - - getTaskId(task) { - return task.meta[TASK_ID][0] - } - - getTaskName(task) { - const ext = path.extname(task.file.path) - return path.basename(task.file.path, ext) - } - - getTasks(filter) { - filter = filter ? ` AND ${filter}` : '' - return this.project.getAllCards('tags=task' + filter) - } - - getTask(taskId) { - return this.project.getCards(`tags=task AND meta.${TASK_ID}="${taskId}"`)[0] - } - - getStory() { - return this.project.getAllCards(`tags=story`)[0] - } - - get name() { - return this.project.name - } - -} - -StoryProject.createAndInit = async function(projectPath, config, storyId, ApplicationContext) { - return (await (new StoryProject(projectPath, config, storyId, ApplicationContext)).init()) -} - -module.exports = StoryProject \ No newline at end of file diff --git a/lib/cli/prompts/MarkdownPlanPrompt.js b/lib/cli/prompts/MarkdownPlanPrompt.js deleted file mode 100644 index 4e9561e5..00000000 --- a/lib/cli/prompts/MarkdownPlanPrompt.js +++ /dev/null @@ -1,16 +0,0 @@ -const defaultStory = `# Story Title - -Story description -` -module.exports.promptForMarkdownPlan = async function () { - const editor = (await import('@inquirer/editor')).default - - return await editor({ - message: 'Enter the story title and description markdown', - validate: function (text) { - if (!text) return 'Please enter a description' - return true - }, - default: defaultStory - }); -} \ No newline at end of file diff --git a/lib/plugins/plugin-manager.js b/lib/plugins/plugin-manager.js index ac961e37..845da630 100644 --- a/lib/plugins/plugin-manager.js +++ b/lib/plugins/plugin-manager.js @@ -11,7 +11,7 @@ module.exports = class PluginManager extends Emitter { constructor(project) { super() this.project = project - this.defaultPlugins = ['./epic-plugin', './extension-plugin', './cli-backlog-plugin'] + this.defaultPlugins = ['./epic-plugin', './extension-plugin'] this.pluginsMap = {} this.pluginPath = _path.join(project.path, '.imdone', 'plugins') this.onDevChange = debounce(this.onDevChange.bind(this), 1000) From a3dad942a9217f3249ba47d58308ae490619d7c9 Mon Sep 17 00:00:00 2001 From: Jesse Piascik Date: Fri, 13 Sep 2024 18:49:11 -0400 Subject: [PATCH 2/6] Update version --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 78fbf762..62fe6481 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "imdone-core", - "version": "1.45.0", + "version": "1.46.0", "description": "imdone-core", "main": "index.js", "scripts": { From 79bc0d8145da54785eefe028b2b29b25bea95c98 Mon Sep 17 00:00:00 2001 From: Jesse Piascik Date: Thu, 26 Sep 2024 16:16:04 -0400 Subject: [PATCH 3/6] provide interpolated title --- lib/card.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/card.js b/lib/card.js index 8fe683fb..4ac6700f 100644 --- a/lib/card.js +++ b/lib/card.js @@ -327,7 +327,9 @@ module.exports = function newCard(task, _project, dontParse) { ) const formattedRawMarkdown = this.formatContent(rawMarkdown, true).content const { html, truncHtml } = this.getHtml(content) + const title = this.format(this.text, true) return { + title, html, truncHtml, encodedText, From 1f83a3361fde89f634256f8b23720b85a899c3cb Mon Sep 17 00:00:00 2001 From: Jesse Piascik Date: Mon, 30 Sep 2024 16:06:54 -0400 Subject: [PATCH 4/6] Add kudosProbability to migrate config --- lib/migrate-config.js | 7 +- lib/plugins/cli-backlog-plugin.js | 152 ------------------------------ 2 files changed, 6 insertions(+), 153 deletions(-) delete mode 100644 lib/plugins/cli-backlog-plugin.js diff --git a/lib/migrate-config.js b/lib/migrate-config.js index a94d30fd..53944405 100644 --- a/lib/migrate-config.js +++ b/lib/migrate-config.js @@ -126,7 +126,12 @@ module.exports = function (repo, defaultSettings = defaultSettingsObject) { // make sure all lists have an id config.lists.forEach((list) => { if (!list.id || (list.id + "").length < 4) list.id = uniqid() - }) + }) + + // Make sure kudosProbability is set + if (!config.settings.kudosProbability && config.settings.kudosProbability !== 0) { + config.settings.kudosProbability = 0.33 + } const defaultCardsSettings = _cloneDeep(defaultSettings.cards) const cardsSettings = _cloneDeep(config.settings.cards) diff --git a/lib/plugins/cli-backlog-plugin.js b/lib/plugins/cli-backlog-plugin.js deleted file mode 100644 index e2f2a355..00000000 --- a/lib/plugins/cli-backlog-plugin.js +++ /dev/null @@ -1,152 +0,0 @@ -const Plugin = require('imdone-api') -const eol = require('eol') -const _path = require('path') -const { - dirname -} = require('path') -const { rmdir } = require('fs').promises -const { - UNGROUPED_TASKS, - STORY_ID, - ORDER -} = require('../cli/domain/BacklogProject').constants -const { newStoryProject } = require('../cli/ProjectConfigFactory') - - -const generateRandomString = (length) => { - let result = ''; - const characters = - 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; - const charactersLength = characters.length; - for (let i = 0; i < length; i++) { - result += characters.charAt(Math.floor(Math.random() * charactersLength)); - } - return result; -}; - -const PROJECT_TYPES = { - BACKLOG_PROJECT: 'BACKLOG_PROJECT', - STORY_PROJECT: 'STORY_PROJECT' -} - -module.exports = class CliBacklogPlugin extends Plugin { - get projectType() { - return this.getSettings().projectType - } - - get isStoryProject() { - return this.projectType === PROJECT_TYPES.STORY_PROJECT - } - - get isBacklogProject() { - return this.projectType === PROJECT_TYPES.BACKLOG_PROJECT - } - - get cardActionsMethod() { - return this.isStoryProject - ? this.getStoryProjectCardActions - : this.isBacklogProject - ? this.getBacklogProjectCardActions - : () => [] - } - - get boardActionsMethod() { - return this.isStoryProject - ? this.getStoryProjectBoardActions - : this.isBacklogProject - ? this.getBacklogProjectBoardActions - : () => [] - - } - - async onBeforeAddTask({path, list, meta, tags, contexts, content}) { - if (!this.isBacklogProject) return {path, content, meta, tags, contexts} - const project = this.project - const storyId = project.sanitizeFileName(eol.split(content)[0]) - const storyProject = await newStoryProject({projectPath: project.path, storyId}) - path = _path.join(storyProject.path, 'README.md') - meta.push({key: STORY_ID, value: storyId}) - meta.push({key: ORDER, value: 0}) - return {path, content, meta, tags, contexts} - } - - async onAfterDeleteTask(task) { - if (!this.isBacklogProject) return - const project = this.project - await rmdir(dirname(task.fullPath), {recursive: true}) - } - - getStoryProjectBoardActions() { - const project = this.project - - const groups = [...new Set( - project.getCards('meta.group = *').map((card) => card.meta.group && card.meta.group[0]) - )].map(group => { - const name = group - const value = `"${group}"` - return { name, value} - }) - const groupActions = [{name: 'All tasks', value: '*'}, ...groups].map(({name, value}) => { - const filterValue = encodeURIComponent(`meta.group = ${value} and tags = task`) - return { - name, - action: function () { - project.openUrl(`imdone://active.repo?filter=${filterValue}`) - } - } - }) - const storyId = project.getAllCards('meta.story-id = *')[0].meta['story-id'][0] - const storiesDir = _path.dirname(project.path) - const backlogDir = _path.dirname(storiesDir) - const showStoryAction = { - name: 'Show story', - action: function () { - const query = encodeURIComponent(`allMeta.story-id="${storyId}"`) - project.openUrl(`imdone://${backlogDir}?filter=${query}`) // imdone:///Users/jesse/projects/imdone-projects/imdone-core/backlog?filter=allMeta.story-id="Board-action-to-show-backlog-from-story-project" - } - } - - return [showStoryAction, ...groupActions] - } - - getBacklogProjectBoardActions() { - return [] - } - - getStoryProjectCardActions(card) { - return [] - } - - getBacklogProjectCardActions(card) { - const fullPath = card.fullPath - const project = this.project - const storyProjectPath = dirname(fullPath) - const title = `Open story tasks` - return card.tags.includes('story') ? [ - { - action: function () { - project.openUrl(`imdone://${storyProjectPath}`) - }, - pack: 'fas', - icon: 'check-square', - title - } - ] : [] - } - - getCardActions(card) { - return this.cardActionsMethod(card) - } - - getBoardActions() { - return this.boardActionsMethod() - } - - getCardProperties(task) { - return { - sid: generateRandomString(5), - projectName: this.project.name, - ungroupedTasks: UNGROUPED_TASKS - } - } -} \ No newline at end of file From feb1b43888c9fcb58cb047511534bd349b3c0436 Mon Sep 17 00:00:00 2001 From: Jesse Piascik Date: Wed, 9 Oct 2024 11:57:02 -0400 Subject: [PATCH 5/6] Add ability to archive completed cards --- lib/adapters/file-gateway.js | 17 +++++++++++ lib/config.js | 12 ++++++++ lib/file.js | 1 + lib/mixins/repo-fs-store.js | 3 +- lib/plugins/archive-plugin.js | 54 +++++++++++++++++++++++++++++++++++ lib/plugins/plugin-manager.js | 8 ++++-- lib/project.js | 23 +++------------ lib/repository.js | 18 +++++++----- test/repos/repo3/deletions.md | 1 + test/repository-spec.js | 6 ++++ 10 files changed, 114 insertions(+), 29 deletions(-) create mode 100644 lib/plugins/archive-plugin.js diff --git a/lib/adapters/file-gateway.js b/lib/adapters/file-gateway.js index b811fe2c..2cd6d913 100644 --- a/lib/adapters/file-gateway.js +++ b/lib/adapters/file-gateway.js @@ -78,6 +78,7 @@ module.exports = { console.warn('sync call') return fs.readdirSync(path, { withFileTypes: true }) }, + sanitizeFileName(fileName, replaceSpacesWith) { // Remove markdown emoji fileName = fileName.replace(/:\w+:/g, '').trim() @@ -85,5 +86,21 @@ module.exports = { if (replaceSpacesWith) fileName = fileName.replace(/ /g, replaceSpacesWith) return fileName + }, + + preparePathForWriting(path) { + let stat = this.statSync(path) + if (!stat) { + let { dir, ext } = _path.parse(path) + if (!ext) { + dir = path + } + this.mkdirpSync(dir) + stat = this.statSync(path) + } + return { + isFile: stat && stat.isFile(), + isDirectory: stat && stat.isDirectory() + } } } diff --git a/lib/config.js b/lib/config.js index 656b5df5..594d7d97 100644 --- a/lib/config.js +++ b/lib/config.js @@ -179,6 +179,18 @@ class Config { return _get(this, 'settings.cards.maxLines', 1) } + get archiveFolder() { + return _get(this, 'settings.cards.archiveFolder', '') + } + + get archiveCompleted() { + return _get(this, 'settings.cards.archiveCompleted', false) + } + + get doneList() { + return this.getDoneList() + } + get blankLinesToEndTask() { let blankLinesToEndTask = Math.round( _get(this, 'settings.cards.blankLinesToEndTask', 1) diff --git a/lib/file.js b/lib/file.js index 13551f48..dde97a34 100644 --- a/lib/file.js +++ b/lib/file.js @@ -74,6 +74,7 @@ function File(opts) { this.tasks = [] this.isDir = false this.lineCount = 0 + this.deleted = false } } util.inherits(File, events.EventEmitter) diff --git a/lib/mixins/repo-fs-store.js b/lib/mixins/repo-fs-store.js index b492e737..16a4b2cf 100644 --- a/lib/mixins/repo-fs-store.js +++ b/lib/mixins/repo-fs-store.js @@ -311,7 +311,8 @@ function mixin(repo, fs = require('fs')) { cb = tools.cb(cb) if (!File.isFile(file)) return cb(new Error(ERRORS.NOT_A_FILE)) - + if (file.deleted) return cb(null, file) + var filePath = repo.getFullPath(file) if (!/\.\.(\/|\\)/.test(file.path)) { diff --git a/lib/plugins/archive-plugin.js b/lib/plugins/archive-plugin.js new file mode 100644 index 00000000..093a5fd6 --- /dev/null +++ b/lib/plugins/archive-plugin.js @@ -0,0 +1,54 @@ +const Plugin = require('imdone-api') +const path = require('path') + +module.exports = class ArchivePlugin extends Plugin { + constructor(project) { + super(project) + } + + get config() { + return this.project.config + } + + get fileGateway() { + return this.project.fileGateway + } + + onTaskUpdate(task) { + if (this.taskShouldBeArchived(task)) { + this.archiveTask(task) + } + } + + taskShouldBeArchived (task, config = this.config) { + return !task.meta.archived && config && config.archiveCompleted && task.list === config.doneList + } + + // Create a new method in File that will move completed tasks to a file in the archive directory. Draw a mermaid diagram to show the flow of the method. Move the archived task to a new file in the archive directory. The archive directory is defined in config.settings.archiveFolder. The new file should be named with the first line of the task (the task.text) and it should be a markdown file. The task should be removed from the file it was in. The new file should be created if it doesn't exist. If the task is not completed, the method should return the task. If the task is completed, the method should return the new file path. + archiveTask (task, config = this.config) { + const archiveFolder = config.archiveFolder + const fileDir = path.dirname(task.relPath) + + if (fileDir.startsWith(archiveFolder)) return + + const fileName = this.fileGateway.sanitizeFileName(`${task.text}.md`, config.replaceSpacesWith) + const newPath = path.join(this.project.path, archiveFolder, fileDir, fileName) + + const taskId = task.id + + this.project.addMetadata(task, 'archived', 'true') + .then((file) => { + const task = file.getTask(taskId) + const taskContent = `#${task.list} ${task.content}` + + this.fileGateway.preparePathForWriting(newPath) + this.fileGateway.writeFileSync(newPath, taskContent) + + this.project.deleteTask(task) + }) + .catch((err) => { + console.log('Exception adding archived meta', err) + }) + } + +} \ No newline at end of file diff --git a/lib/plugins/plugin-manager.js b/lib/plugins/plugin-manager.js index 845da630..2815c58b 100644 --- a/lib/plugins/plugin-manager.js +++ b/lib/plugins/plugin-manager.js @@ -11,7 +11,11 @@ module.exports = class PluginManager extends Emitter { constructor(project) { super() this.project = project - this.defaultPlugins = ['./epic-plugin', './extension-plugin'] + this.defaultPlugins = [ + './archive-plugin', + './epic-plugin', + './extension-plugin' + ] this.pluginsMap = {} this.pluginPath = _path.join(project.path, '.imdone', 'plugins') this.onDevChange = debounce(this.onDevChange.bind(this), 1000) @@ -19,7 +23,7 @@ module.exports = class PluginManager extends Emitter { } async startDevMode() { - if (this.project.config.devMode && !this.watcher) { + if (this.project && this.project.config.devMode && !this.watcher) { if (!(await this.exists(this.pluginPath))) await fs.promises.mkdir(this.pluginPath) this.watcher = sane(this.pluginPath, { diff --git a/lib/project.js b/lib/project.js index f01c28c7..ebb7ab1c 100644 --- a/lib/project.js +++ b/lib/project.js @@ -47,6 +47,7 @@ module.exports = class WorkerProject extends Project { super() this.repo = repo this.innerFilter = '' + this.fileGateway = fileGateway } // HACK:-50 To handle circular dependency issue with file @@ -371,13 +372,13 @@ module.exports = class WorkerProject extends Project { async updateCardContent(task, content) { return new Promise((resolve, reject) => { - this.repo.modifyTaskFromContent(task, content, (err) => { + this.repo.modifyTaskFromContent(task, content, (err, file) => { if (err) { console.error(err) reject(err) } this.emitUpdate() - resolve() + resolve(file) }) }) } @@ -416,22 +417,6 @@ module.exports = class WorkerProject extends Project { this.emit('project.saveFile', { file: filePath, content }) } - preparePathForWriting(path) { - let stat = fileGateway.statSync(path) - if (!stat) { - let { dir, ext } = _path.parse(path) - if (!ext) { - dir = path - } - fileGateway.mkdirpSync(dir) - stat = fileGateway.statSync(path) - } - return { - isFile: stat && stat.isFile(), - isDirectory: stat && stat.isDirectory() - } - } - /* * @param {Object} opts - The options object * @param {String} opts.list - The list name to add the card to @@ -445,7 +430,7 @@ module.exports = class WorkerProject extends Project { if (!path || !_path.parse(path).ext) path = this.getNewCardsFile({title}) path = this.getFullPath(path) - const { isFile, isDirectory } = this.preparePathForWriting(path) + const { isFile, isDirectory } = fileGateway.preparePathForWriting(path) if (!template) template = this.getNewCardTemplate(path, isFile) diff --git a/lib/repository.js b/lib/repository.js index 3775f1c4..b1323c70 100644 --- a/lib/repository.js +++ b/lib/repository.js @@ -622,12 +622,14 @@ Repository.prototype.readFile = function (file, cb) { const checksum = this.checksum || function () {} cb = tools.cb(cb) if (!File.isFile(file)) return cb(new Error(ERRORS.NOT_A_FILE)) + if (file.deleted) return cb(null, file) var self = this var currentChecksum = file.checksum - if (!/\.\.(\/|\\)/.test(file.path)) { + const filePath = file.path + if (!/\.\.(\/|\\)/.test(filePath)) { this.readFileContent(file, (err, file) => { - if (err) return cb(new Error('Unable to read file:' + file)) + if (err) return cb(new Error('Unable to read file:' + filePath)) // BACKLOG:-40 Allow user to assign a module for content transformation file.checksum = checksum(file.getContent()) file.updated = currentChecksum !== file.checksum @@ -966,13 +968,15 @@ Repository.prototype.deleteTask = function (task, cb) { if (!cb) throw new Error('task, callback required') var self = this var file = self.getFileForTask(task) + if (!file) return cb(null) const execute = function (file) { file.deleteTask(task, self.getConfig()) - if ( - file.getContentForFile().trim() === '' && - self.config.journalType === constants.JOURNAL_TYPE.NEW_FILE - ) { - return self.deleteFile(file.path, cb) + if (file.getContentForFile().trim() === '') { + if (file.deleted) return + console.log('Deleting empty file:', file.path) + self.deleteFile(file.path, cb) + file.deleted = true + return } self.writeAndExtract(file, true, cb) } diff --git a/test/repos/repo3/deletions.md b/test/repos/repo3/deletions.md index 6e42d024..41ab1e87 100644 --- a/test/repos/repo3/deletions.md +++ b/test/repos/repo3/deletions.md @@ -2,6 +2,7 @@ - a comment line - another comment line + ### Some content that should stay This should be left behind diff --git a/test/repository-spec.js b/test/repository-spec.js index 5dbe29a3..328314f1 100644 --- a/test/repository-spec.js +++ b/test/repository-spec.js @@ -616,6 +616,9 @@ describe('Repository', function () { function iter(next) { let task = todos.pop() const file = repo3.getFileForTask(task) + if (!file) { + return next() + } repo3.deleteTask(task, function (err) { if (err) return next(err) repo3.readFile(file, function (err) { @@ -646,6 +649,9 @@ describe('Repository', function () { function iter(next) { let task = todos.pop() const file = repo3.getFileForTask(task) + if (!file) { + return next() + } repo3.deleteTask(task, function (err) { if (err) return next(err) repo3.readFile(file, function (err) { From 389ecd408817be365ca9f3ca12933adfad99c970 Mon Sep 17 00:00:00 2001 From: Jesse Piascik Date: Thu, 10 Oct 2024 09:28:23 -0400 Subject: [PATCH 6/6] Add archive plugin --- lib/adapters/file-gateway.js | 5 +++ lib/migrate-config.js | 17 ++++++-- lib/plugins/archive-plugin.js | 50 +++++++++++---------- lib/project.js | 82 ++++++++++++++++++++--------------- 4 files changed, 91 insertions(+), 63 deletions(-) diff --git a/lib/adapters/file-gateway.js b/lib/adapters/file-gateway.js index 2cd6d913..52d08c9c 100644 --- a/lib/adapters/file-gateway.js +++ b/lib/adapters/file-gateway.js @@ -30,6 +30,11 @@ module.exports = { return fs.readFileSync.apply({}, args) }, + appendFileSync(...args) { + console.warn('sync call') + return fs.appendFileSync.apply({}, args) + }, + writeFileSync(...args) { console.warn('sync call') return fs.writeFileSync.apply({}, args) diff --git a/lib/migrate-config.js b/lib/migrate-config.js index 53944405..6efe20f1 100644 --- a/lib/migrate-config.js +++ b/lib/migrate-config.js @@ -36,6 +36,9 @@ module.exports = function (repo, defaultSettings = defaultSettingsObject) { config.code.include_lists.push(list.name) }) + if (!config.settings.cards) { + config.settings.cards = {} + } // metaSep if (config.settings.metaSep) { config.settings.cards.metaSep = config.settings.metaSep @@ -47,14 +50,20 @@ module.exports = function (repo, defaultSettings = defaultSettingsObject) { delete config.settings.defaultList } - if (!config.settings.cards) { - config.settings.cards = {} - } - if (!config.settings.cards.defaultList) { config.settings.cards.defaultList = lists ? lists[0] : '' } + // archiveCompleted + if (config.settings.cards.archiveCompleted === undefined) { + config.settings.cards.archiveCompleted = false + } + + // archiveFolder + if (!config.settings.cards.archiveFolder) { + config.settings.cards.archiveFolder = config.settings.journalPath ? _path.join(config.settings.journalPath, 'archive') : 'archive' + } + // showTagsAndMeta if (config.settings.cards.showTagsAndMeta === undefined) { config.settings.cards.showTagsAndMeta = true diff --git a/lib/plugins/archive-plugin.js b/lib/plugins/archive-plugin.js index 093a5fd6..23cdefc7 100644 --- a/lib/plugins/archive-plugin.js +++ b/lib/plugins/archive-plugin.js @@ -14,17 +14,26 @@ module.exports = class ArchivePlugin extends Plugin { return this.project.fileGateway } - onTaskUpdate(task) { - if (this.taskShouldBeArchived(task)) { - this.archiveTask(task) + onBeforeBoardUpdate() { + const config = this.config + if (!config || !config.archiveCompleted || this.archivingTasks) return + this.archivingTasks = true + const cards = this.project.getAllCards(`meta.archived != "true" and list = ${config.doneList}`) + const messageUser = cards.length > 1 + if (!cards.length) { + this.archivingTasks = false + return + } else { + if (messageUser) this.project.snackBar({message: `Archiving ${cards.length} card(s)...`}) } + cards.forEach(card => this.archiveTask(card)) + if (messageUser) this.project.snackBar({message: ` Done archiving ${cards.length} card(s). Deleting original(s)...`}) + this.project.deleteTasks(cards).then(() => { + this.archivingTasks = false + if (messageUser) this.project.snackBar({message: `Done deleting original card(s).`}) + }) } - taskShouldBeArchived (task, config = this.config) { - return !task.meta.archived && config && config.archiveCompleted && task.list === config.doneList - } - - // Create a new method in File that will move completed tasks to a file in the archive directory. Draw a mermaid diagram to show the flow of the method. Move the archived task to a new file in the archive directory. The archive directory is defined in config.settings.archiveFolder. The new file should be named with the first line of the task (the task.text) and it should be a markdown file. The task should be removed from the file it was in. The new file should be created if it doesn't exist. If the task is not completed, the method should return the task. If the task is completed, the method should return the new file path. archiveTask (task, config = this.config) { const archiveFolder = config.archiveFolder const fileDir = path.dirname(task.relPath) @@ -34,21 +43,16 @@ module.exports = class ArchivePlugin extends Plugin { const fileName = this.fileGateway.sanitizeFileName(`${task.text}.md`, config.replaceSpacesWith) const newPath = path.join(this.project.path, archiveFolder, fileDir, fileName) - const taskId = task.id - - this.project.addMetadata(task, 'archived', 'true') - .then((file) => { - const task = file.getTask(taskId) - const taskContent = `#${task.list} ${task.content}` - - this.fileGateway.preparePathForWriting(newPath) - this.fileGateway.writeFileSync(newPath, taskContent) - - this.project.deleteTask(task) - }) - .catch((err) => { - console.log('Exception adding archived meta', err) - }) + const content = this.project.addMetaToContent( + [ + { key: 'archived', value: 'true'} + ], + task.content + ) + const taskContent = `${task.beforeText}#${task.list} ${content}` + + this.fileGateway.preparePathForWriting(newPath) + this.fileGateway.appendFileSync(newPath, taskContent) } } \ No newline at end of file diff --git a/lib/project.js b/lib/project.js index ebb7ab1c..8c230322 100644 --- a/lib/project.js +++ b/lib/project.js @@ -50,6 +50,42 @@ module.exports = class WorkerProject extends Project { this.fileGateway = fileGateway } + init(cb) { + this.repo.project = this + + this.pluginManager = new PluginManager(this) + this.pluginManager.on('plugin-installed', () => this.emitUpdate()) + this.pluginManager.on('plugin-uninstalled', () => this.emitUpdate()) + this.pluginManager.on('plugins-reloaded', () => this.emitUpdate()) + + EVENTS.forEach((event) => { + this.repo.on(event, (data) => onChange(this, event, data)) + }) + + const promise = new Promise((resolve, reject) => { + this.repo.init((err, files) => { + if (err) { + if (cb) cb(err) + else reject(err) + return + } + this.pluginManager + .startDevMode() + .then(() => { + if (cb) cb(null, files) + else resolve(files) + }) + .catch(err => { + console.log('Error on starting dev mode', err) + if (cb) cb(err) + else reject(err) + }) + }) + }) + + if (!cb) return promise + } + // HACK:-50 To handle circular dependency issue with file toJSON() { return { path: this.path } @@ -115,42 +151,6 @@ module.exports = class WorkerProject extends Project { } } - init(cb) { - this.repo.project = this - this.pluginManager = new PluginManager(this) - this.pluginManager.on('plugin-installed', () => this.emitUpdate()) - this.pluginManager.on('plugin-uninstalled', () => this.emitUpdate()) - this.pluginManager.on('plugins-reloaded', () => this.emitUpdate()) - - EVENTS.forEach((event) => { - this.repo.on(event, (data) => onChange(this, event, data)) - }) - - - const promise = new Promise((resolve, reject) => { - this.repo.init((err, files) => { - if (err) { - if (cb) cb(err) - else reject(err) - return - } - this.pluginManager - .startDevMode() - .then(() => { - if (cb) cb(null, files) - else resolve(files) - }) - .catch(err => { - console.log('Error on starting dev mode', err) - if (cb) cb(err) - else reject(err) - }) - }) - }) - - if (!cb) return promise - } - emit() {} emitUpdate() { @@ -545,6 +545,16 @@ module.exports = class WorkerProject extends Project { }) } + async deleteTasks(tasks) { + return new Promise((resolve, reject) => { + this.repo.deleteTasks(tasks, async (err) => { + if (err) return reject(err) + resolve() + }) + }) + + } + setFilter(filter) { this.emit('project.filter', { filter }) }