imdone-cli 0.1.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.
@@ -0,0 +1,9 @@
1
+ -----BEGIN PUBLIC KEY-----
2
+ MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAztGTqz0skWsaknTnXIEL
3
+ 7Vi/BAAnJZf7NH4mAypIf4Dn6YZjwjdpNfdW+RzWxwaW0oyzGyIf2uuRrScAcnka
4
+ 0uaAWDjHqB7ghiqm41/5GmIbtmXWTa2QHamhYak5Wa/grprrwzaRY4dfHE/3n1N+
5
+ B4lbeWw8Ga5hiRAWnKT9SkkyaYdHICeiewo03xSjkMs4+tg9kiHl+v32vRA7lVfz
6
+ u4+GaAWjVS8/FRLF98wZWi0xCnaHLvdTIOl1PtUZXnrN5Bt8lcKwwqjxIRLiFn8B
7
+ GUjIHIEMDwoxzfd+y6jWH9qEMo7qfJGyiti6w6EmHfz/UoOmDAh4lWq1kJNBqkAQ
8
+ FwIDAQAB
9
+ -----END PUBLIC KEY-----
package/package.json ADDED
@@ -0,0 +1,35 @@
1
+ {
2
+ "name": "imdone-cli",
3
+ "version": "0.1.0",
4
+ "author": "Jesse Piascik",
5
+ "description": "An imdone cli that automates your task updates with markdown files.",
6
+ "main": "src/index.js",
7
+ "type": "module",
8
+ "bin": {
9
+ "imdone": "dist/index.cjs"
10
+ },
11
+ "scripts": {
12
+ "prepublishOnly": "npm --prefix ../ install && npm --prefix ../ run build && npm i && npm test && npm run build",
13
+ "link": "npm link ../../imdone-core-2 ../../imdone-api",
14
+ "build": "node esbuild.config.js && cp public/* dist/",
15
+ "coverage": "vitest --run --coverage",
16
+ "test": "vitest --run",
17
+ "test-ci": "vitest --run"
18
+ },
19
+ "license": "ISC",
20
+ "dependencies": {
21
+ "chalk": "^5.4.1",
22
+ "commander": "^13.1.0",
23
+ "dotenv": "16.4.5",
24
+ "imdone-core": "^2.0.8",
25
+ "inquirer": "^12.5.2",
26
+ "jsonwebtoken": "^9.0.2",
27
+ "ora": "^8.2.0",
28
+ "simple-git": "^3.27.0"
29
+ },
30
+ "devDependencies": {
31
+ "esbuild": "^0.25.2",
32
+ "esbuild-plugin-copy": "^2.1.1",
33
+ "vitest": "^3.1.1"
34
+ }
35
+ }
@@ -0,0 +1,11 @@
1
+ #!/bin/bash
2
+
3
+ set -e
4
+
5
+ # Generate a 2048-bit RSA private key (unencrypted)
6
+ openssl genpkey -algorithm RSA -out ./jwt-private.pem -pkeyopt rsa_keygen_bits:2048
7
+
8
+ # Extract the public key from the private key
9
+ openssl rsa -pubout -in ./jwt-private.pem -out jwt-public.pem
10
+
11
+ echo "✅ RSA 2048-bit key pair generated in ./keys/"
@@ -0,0 +1,28 @@
1
+ -----BEGIN PRIVATE KEY-----
2
+ MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQC/zPuZfQQyke5+
3
+ KUs2AeKt4BMbO0TZgSt+1pLYzXXhtKj5qVn0GUY1QS03F3/dksUS+oND9jmyxBoK
4
+ 0IdLUKa1pX66rM8nTb3fMYk+wVAn+oBK1hKSv/pCWkrw/oN19aAzl9yf5+BWBym1
5
+ JkoXYlU3607PPWzMZ26lZ/xGUpMsKHQmdX2Enk7ZfoHdT7xhnUkP+dQ5Qdf2rUyf
6
+ Y58Bb7yJtWlo00O0bs4+oBH4d5N+Ft/Qx3LxHKlDL374G+28fImT2tfAiYYs9Ash
7
+ diiecv1RTjNxtVFoo34Vu8x1eQMsoWLjtQKdMB5UxmmobwiYfULwyweCXKJuvo9L
8
+ MQK2zQRlAgMBAAECggEADxL+dDf/4DSZdKdsrpQz21wIzGNaXCjbgQa+uFTfjqtq
9
+ +2I8vGNPIgjbE6Egbsrl+8GeFvWPX9YrQM3WUExIqe6VmPjTeCCTS2sigQDi+p1k
10
+ v1z3TTrS4aukh6/NJ7R0EJH/KD8qYcCC3eaiPJfBIFm5Ui7e8eqwJsZXLLS5Nede
11
+ CjkdCTbHA3hSJIplEFSiTDrV7TTg+s9PwZV0GboOmUS9zmFBWRr1VTsPit5YbRty
12
+ VmvI89XDQROMimMmrL9v9dK0TeqzFABDi+Sad5lR5udxov9BfZWUEaBcwmpxh7kt
13
+ TaOPTXAw/wv9X05rBr0dmkoUEhyoeIk6aELyqSD6sQKBgQDzxsItdxYAoPufort8
14
+ D0qEdYlahISVnH23Yv7Q/+Hr5uZs7WG6MPYiPe5nSgYQcnA/IgXB2yebbM1vm6dD
15
+ nHIXYVmw/pB/1rIlqbxlvOf7eUiQ39FVp4h2LWSeY0aI0dg+XL6cpULf5n0+bJCD
16
+ /i01d6NcEPOxYPfbJsEFgckM0QKBgQDJawmE/lhxsgCcR+WxxDeGciZQIOydjr4/
17
+ caN062qijxXB6lZAi9fg0EcReuVmsXLw25TOHi9ABON6FftdbagTk9lgrcTWo21F
18
+ XxD1AeJoQUwlort3AnJ8SS/1b1C2KX8S3hI4vX2RXuqUGgubBdW4Y5aC0KOZAqhz
19
+ Yb0xGqRTVQKBgDxLwyeftu984gAALkNnPNU6hTjAYlLnHClJ8SEcyXKh8AitRmjZ
20
+ R2f8zYT6yDk1NRJIhggG/urwpHeglmSgw4+I4rhmnrMgFXw/WXwIl5CZ1RsQYSTA
21
+ hX5FiAetInsg/E2gfv0b20iqJ/xSugQL0H7TErLo9n2/ME8ibMfB7EqhAoGAbsUI
22
+ 5TUj2tMz9r6rmcn5Z10bqPGSb0vzYNzUMhbN/DyIkK6ZZMIDpUWl7/0QcBuixMdd
23
+ 3MVI4wJNP/Ua2lTWHH4xDDREjm4uR/pyTuwMYZ2XjRMj2d1krOlrEKV5U9UaZ3vt
24
+ tXqwtePnSi/Qth7NXKyYN7UaY6nMjjfwXjLysEkCgYB4b+1gVzuPWBZsJbo/yX2Q
25
+ HWjdwL9EgbaFHvqI6DVoMBMux+KB5KKVTn+yRS2NAR7fxdYsVeEz+z8ftfVbXRpt
26
+ 7dpPJxBe+MpoOojGkpg8UC1/SIwG2Qtg4QDgdCK2QS8JbeRpziK0GyLIfiSG5i4R
27
+ CX69OWZrIxmbTy9Vun0nCg==
28
+ -----END PRIVATE KEY-----
@@ -0,0 +1,9 @@
1
+ -----BEGIN PUBLIC KEY-----
2
+ MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAv8z7mX0EMpHufilLNgHi
3
+ reATGztE2YErftaS2M114bSo+alZ9BlGNUEtNxd/3ZLFEvqDQ/Y5ssQaCtCHS1Cm
4
+ taV+uqzPJ0293zGJPsFQJ/qAStYSkr/6QlpK8P6DdfWgM5fcn+fgVgcptSZKF2JV
5
+ N+tOzz1szGdupWf8RlKTLCh0JnV9hJ5O2X6B3U+8YZ1JD/nUOUHX9q1Mn2OfAW+8
6
+ ibVpaNNDtG7OPqAR+HeTfhbf0Mdy8RypQy9++BvtvHyJk9rXwImGLPQLIXYonnL9
7
+ UU4zcbVRaKN+FbvMdXkDLKFi47UCnTAeVMZpqG8ImH1C8MsHglyibr6PSzECts0E
8
+ ZQIDAQAB
9
+ -----END PUBLIC KEY-----
@@ -0,0 +1,27 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { generateLicense, verifyLicense } from "../license";
3
+ import { fileURLToPath } from 'url';
4
+ import path from 'path';
5
+
6
+ const __filename = fileURLToPath(import.meta.url);
7
+ const __dirname = path.dirname(__filename);
8
+
9
+ describe("License Generation and Verification", () => {
10
+ const privateKeyPath = path.join(__dirname, 'jwt-private.pem');
11
+ const publicKeyPath = path.join(__dirname, 'jwt-public.pem');
12
+ const payload = {
13
+ name: "Test User",
14
+ email: "test@test.com",
15
+ org: "Test Organization",
16
+ plan: "premium",
17
+ expiresIn: "30d",
18
+ };
19
+
20
+ it("should generate a valid license", async () => {
21
+ const license = await generateLicense(payload, privateKeyPath);
22
+ expect(license).toBeDefined();
23
+ expect(typeof license).toBe("string");
24
+ const decoded = await verifyLicense(license, publicKeyPath);
25
+ expect(decoded.valid).toBe(true);
26
+ });
27
+ })
@@ -0,0 +1,21 @@
1
+ import { envExists as envExistsAdapter, appendToEnv, initEnv } from "../../../src/adapters/env";
2
+ import { logger } from "imdone-core/lib/adapters/logger.js";
3
+ export async function envExists(projectPath) {
4
+ return envExistsAdapter(projectPath)
5
+ }
6
+
7
+ export async function storeJiraCredentials(projectPath, jiraUsername, jiraApiToken) {
8
+ await appendToEnv(projectPath, 'JIRA_USERNAME', jiraUsername)
9
+ await appendToEnv(projectPath, 'JIRA_TOKEN', jiraApiToken)
10
+ logger.info('Stored Jira credentials in .env file')
11
+ }
12
+
13
+ export async function storeLicense(projectPath, license) {
14
+ await appendToEnv(projectPath, 'IMDONE_LICENSE', license)
15
+ logger.info('Stored license in .env file')
16
+ }
17
+
18
+ export async function init(projectPath) {
19
+ await initEnv(projectPath)
20
+ logger.info('Initialized .env file')
21
+ }
@@ -0,0 +1,113 @@
1
+ import { exec } from 'child_process'
2
+ import { simpleGit } from 'simple-git'
3
+ import { constants } from 'imdone-core/lib/constants.js'
4
+ import { TAGS_FILE_PATH } from 'imdone-core/lib/adapters/storage/tags.js'
5
+ import { access, appendFile } from 'fs/promises'
6
+ import path from 'path'
7
+
8
+ const { CONFIG_FILE_YML } = constants
9
+
10
+ export async function isGitRepo(projectDir) {
11
+ try {
12
+ await access(path.join(projectDir, '.git'), constants.R_OK)
13
+ return true
14
+ } catch (error) {
15
+ return false
16
+ }
17
+ }
18
+
19
+ export async function createRepo(projectDir) {
20
+ if (await isGitRepo(projectDir)) {
21
+ return
22
+ }
23
+ gitRepo(projectDir).init()
24
+ }
25
+
26
+ export async function appendToGitignore(projectDir, ignorePath) {
27
+ const gitignorePath = path.join(projectDir, '.gitignore')
28
+ await appendFile(gitignorePath, `${ignorePath}\n`, 'utf8')
29
+ await addAndCommitChanges(projectDir, `Append ${ignorePath} to .gitignore`, [".gitignore"]);
30
+ }
31
+
32
+ export async function isFileModified(projectDir, filePath) {
33
+ // run git diff --name-only <file> || git diff --cached --name-only <file>
34
+ return new Promise((resolve, reject) => {
35
+ exec(`cd "${projectDir}" && git diff --name-only "${filePath}" || git diff --cached --name-only "${filePath}"`, (error, stdout, stderr) => {
36
+ if (error) {
37
+ console.error(`exec error: ${error}`);
38
+ reject(error);
39
+ } else {
40
+ resolve(stdout.trim() !== '');
41
+ }
42
+ });
43
+ });
44
+ }
45
+
46
+ export async function isImdoneInit(projectPath) {
47
+ const git = gitRepo(projectPath)
48
+ const status = await git.status()
49
+ if (status.isClean()) {
50
+ return false
51
+ }
52
+ const { files } = status
53
+ const configFile = files.find(file => file.path === CONFIG_FILE_YML)
54
+ const tagsFile = files.find(file => file.path === TAGS_FILE_PATH)
55
+ return configFile || tagsFile
56
+ }
57
+
58
+ export async function gitIgnoreExists(projectPath, ignorePath) {
59
+ const git = gitRepo(projectPath)
60
+ const ignored = await git.checkIgnore([ignorePath])
61
+
62
+ return ignored.length > 0
63
+ }
64
+
65
+ export async function hasLocalChanges(projectPath) {
66
+ const git = gitRepo(projectPath)
67
+ const status = await git.status()
68
+ return status?.files?.length > 0
69
+ }
70
+
71
+ export async function expectNoChanges(projectPath, message) {
72
+ if (await hasLocalChanges(projectPath)) {
73
+ throw new Error(`There are modified files. Commit or stash your changes before ${message}.`)
74
+ }
75
+ }
76
+
77
+ export async function addAndCommitChanges(projectPath, message, add = ['.']) {
78
+ const git = gitRepo(projectPath)
79
+ const files = await status()
80
+ if (files.length === 0) {
81
+ return
82
+ }
83
+ const toAdd = add.includes('.') ? add : add.filter(file => files.some(f => f.path === file))
84
+ if (toAdd.length === 0) {
85
+ return
86
+ }
87
+ await git.add(toAdd)
88
+ await git.commit(message)
89
+ }
90
+
91
+ export async function stashChanges(projectPath) {
92
+ const git = gitRepo(projectPath)
93
+ await git.stash(['push', '-m', `imdone-with-jira-${new Date().toISOString()}`]);
94
+ }
95
+
96
+ export async function stashPop(projectPath) {
97
+ const git = gitRepo(projectPath)
98
+ await git.stash(['pop']);
99
+ const { files } = await git.status()
100
+ return files
101
+ }
102
+
103
+ export async function status(projectPath) {
104
+ const git = gitRepo(projectPath)
105
+ const { files } = await git.status()
106
+ return files
107
+ }
108
+
109
+ export function gitRepo(projectPath) {
110
+ const git = simpleGit({ baseDir: projectPath })
111
+ return git
112
+ }
113
+
@@ -0,0 +1,79 @@
1
+ export function getImdoneConfigTemplate({name, jiraUrl, jql, jiraProjectKey}) {
2
+ return `keepEmptyPriority: true
3
+ code:
4
+ include_lists:
5
+ - TODO
6
+ - DOING
7
+ - DONE
8
+ - PLANNING
9
+ - FIXME
10
+ - ARCHIVE
11
+ - HACK
12
+ - CHANGED
13
+ - XXX
14
+ - IDEA
15
+ - NOTE
16
+ - REVIEW
17
+ - BACKLOG
18
+ lists:
19
+ - name: TODO
20
+ hidden: false
21
+ ignore: false
22
+ - name: DOING
23
+ hidden: false
24
+ ignore: false
25
+ - name: DONE
26
+ hidden: false
27
+ ignore: true
28
+ settings:
29
+ openIn: code
30
+ openCodeIn: default
31
+ journalType: New File
32
+ journalPath: current-sprint
33
+ newCardSyntax: HASHTAG
34
+ replaceSpacesWith: '-'
35
+ plugins:
36
+ JiraPlugin:
37
+ jql: ${jql}
38
+ issueType: Story
39
+ projectKeys:
40
+ - key: ${jiraProjectKey}
41
+ statuses:
42
+ - jira: To Do
43
+ list: TODO
44
+ - jira: In Progress
45
+ list: DOING
46
+ - jira: Done
47
+ list: DONE
48
+ url: '${jiraUrl}'
49
+ defaultList: DOING
50
+ defaultDirectory: current-sprint
51
+ devMode: false
52
+ journalTemplate: null
53
+ markdownOnly: false
54
+ kudosProbability: 0.33
55
+ name: ${name}
56
+ views: []
57
+ cards:
58
+ colors: []
59
+ template: ''
60
+ trackChanges: false
61
+ metaNewLine: true
62
+ addCompletedMeta: true
63
+ addCheckBoxTasks: false
64
+ doingList: DOING
65
+ doneList: DONE
66
+ tokenPrefix: '#'
67
+ taskPrefix: '#'
68
+ tagPrefix: '#'
69
+ metaSep: ':'
70
+ orderMeta: true
71
+ maxLines: 3
72
+ addNewCardsToTop: true
73
+ showTagsAndMeta: false
74
+ addStartedMeta: false
75
+ defaultList: TODO
76
+ archiveCompleted: true
77
+ archiveFolder: archive
78
+ `
79
+ };
@@ -0,0 +1,71 @@
1
+ import { logger } from 'imdone-core/lib/adapters/logger.js'
2
+ import { createFileSystemProject } from 'imdone-core/lib/project-factory.js'
3
+ import { constants } from 'imdone-core/lib/constants.js'
4
+ import { load, getConfigPath, isImdoneProject } from 'imdone-core/lib/adapters/storage/config.js'
5
+ import { isImdoneInit, addAndCommitChanges } from './git.js'
6
+ import { appendFile } from 'fs/promises'
7
+ import path from 'path'
8
+ import { getImdoneConfigTemplate } from './imdone-config-template.js'
9
+ import JiraPlugin from '../../../main.js'
10
+ import { TAGS_FILE_PATH } from 'imdone-core/lib/adapters/storage/tags.js'
11
+ import { preparePathForWriting, writeFile } from 'imdone-core/lib/adapters/file-gateway.js'
12
+ const { CONFIG_FILE_YML, IGNORE_FILE } = constants
13
+
14
+ export async function getProject(projectPath, feedback = {}) {
15
+ if (!await isImdoneProject(projectPath)) {
16
+ throw new Error(`Project is not initialized. Run "imdone init" to initialize the project.`)
17
+ }
18
+ const config = await load(projectPath)
19
+ const project = createFileSystemProject({
20
+ path: projectPath,
21
+ config
22
+ })
23
+ project.toast = ({message}) => feedback.text = message
24
+ project.snackBar = ({message}) => feedback.text = message
25
+
26
+ project.emit = function(event, opts = {}) {
27
+ const { message, type, duration } = opts
28
+ logger.info(event, { message, type, duration })
29
+ }
30
+ await project.init()
31
+
32
+ if (await isImdoneInit(projectPath)) {
33
+ await addAndCommitChanges(projectPath, 'Initial commit', [CONFIG_FILE_YML, TAGS_FILE_PATH])
34
+ }
35
+
36
+ const jiraPlugin = await project.pluginManager.createPlugin(JiraPlugin)
37
+ const { pullFromJiraAction, pushToJiraAction } = jiraPlugin
38
+ return {
39
+ project,
40
+ jiraPlugin: {
41
+ pullFromJiraAction,
42
+ pushToJiraAction
43
+ }
44
+ }
45
+ }
46
+
47
+ export async function initProject(projectPath, {
48
+ name,
49
+ jiraUrl,
50
+ jiraProjectKey,
51
+ jql
52
+ }) {
53
+ const configContent = getImdoneConfigTemplate({
54
+ name,
55
+ jiraUrl,
56
+ jiraProjectKey,
57
+ jql
58
+ })
59
+
60
+ const configPath = getConfigPath(projectPath)
61
+ await preparePathForWriting(configPath)
62
+ await writeFile(configPath, configContent, 'utf8')
63
+ await getProject(projectPath)
64
+ }
65
+
66
+ export async function appendToImdoneIgnore(projectPath, ignorePath) {
67
+ const imdoneIgnorePath = path.join(projectPath, IGNORE_FILE)
68
+ await appendFile(imdoneIgnorePath, `${ignorePath}\n`, 'utf8')
69
+ await addAndCommitChanges(projectPath, `Append ${ignorePath} to ${IGNORE_FILE}`, [IGNORE_FILE])
70
+ }
71
+
@@ -0,0 +1,83 @@
1
+ import { readFile } from 'node:fs/promises';
2
+ import jwt from 'jsonwebtoken';
3
+ import path from 'node:path';
4
+
5
+ const DIRNAME = path.dirname(import.meta.url);
6
+ const PUBLIC_KEY_PATH = path.join(DIRNAME, 'jwt-public.pem');
7
+
8
+ export const DEFAULT_PAYLOAD = {
9
+ name: undefined,
10
+ email: undefined,
11
+ org: undefined,
12
+ plan: "premium",
13
+ expiresIn: '30d',
14
+ }
15
+
16
+ export async function generateLicense(payload, privateKeyPath) {
17
+ // Apply default values to payload
18
+ const finalPayload = { ...DEFAULT_PAYLOAD, ...payload };
19
+
20
+ // throw an exception if the payload is missing required fields
21
+ const requiredFields = ['name', 'email', 'org', 'plan', 'expiresIn'];
22
+ for (const field of requiredFields) {
23
+ if (!finalPayload[field]) {
24
+ throw new Error(`Missing required field: ${field}`);
25
+ }
26
+ }
27
+
28
+ // Check if the payload has any extra fields
29
+ const extraFields = Object.keys(finalPayload).filter(field => !DEFAULT_PAYLOAD.hasOwnProperty(field));
30
+ if (extraFields.length > 0) {
31
+ throw new Error(`Extra fields found in payload: ${extraFields.join(', ')}`);
32
+ }
33
+
34
+ const { expiresIn } = finalPayload;
35
+ delete finalPayload.expiresIn; // Remove expiresIn from payload
36
+
37
+ const privateKey = await readFile(privateKeyPath, 'utf8');
38
+ return jwt.sign(finalPayload, privateKey, {
39
+ algorithm: 'RS256',
40
+ expiresIn,
41
+ });
42
+ }
43
+
44
+ export async function verifyLicense(token, publicKeyPath = PUBLIC_KEY_PATH) {
45
+ try {
46
+ const publicKey = await readFile(publicKeyPath, 'utf8');
47
+ const decoded = jwt.verify(token, publicKey, {
48
+ algorithms: ['RS256'],
49
+ });
50
+ return { valid: true, data: License.fromPayload(decoded) };
51
+ } catch (err) {
52
+ return { valid: false, reason: err.message };
53
+ }
54
+ }
55
+
56
+ export class License {
57
+ constructor({name, email, org, plan, created, expires}) {
58
+ this.name = name;
59
+ this.email = email;
60
+ this.org = org;
61
+ this.plan = plan;
62
+ this.created = created;
63
+ this.expires = expires;
64
+ }
65
+
66
+ static fromPayload(payload) {
67
+ return new License({
68
+ name: payload.name,
69
+ email: payload.email,
70
+ org: payload.org,
71
+ plan: payload.plan,
72
+ created: new Date(payload.iat * 1000),
73
+ expires: new Date(payload.exp * 1000),
74
+ });
75
+ }
76
+ }
77
+
78
+ export async function validateEnvLicense() {
79
+ const { IMDONE_LICENSE } = process.env;
80
+ return IMDONE_LICENSE
81
+ ? await verifyLicense(IMDONE_LICENSE)
82
+ : { valid: false, reason: 'No license found in environment variables.' };
83
+ }
package/src/bin.js ADDED
@@ -0,0 +1,164 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { Command } from 'commander';
4
+ import ora from 'ora';
5
+ import { pullFromJira, dependencies as pullDeps } from "./usecases/headless-pull-from-jira.js";
6
+ import { pushToJira, dependencies as pushDeps } from "./usecases/headless-push-to-jira.js";
7
+ import { logger, LogLevel } from 'imdone-core/lib/adapters/logger.js';
8
+ import { gitRepoCheck, imdoneProjectCheck, dotEnvCheck, gitIgnoreCheck, promptForConfig, promptForLicense, checkLicense } from './ui/prompt.js';
9
+ import { imdoneInit } from './usecases/init.js';
10
+ import { verifyLicense } from './adapters/license.js';
11
+ import { init as initEnv, storeLicense } from './adapters/env.js';
12
+
13
+ logger.setLevel(process.env.LOG_LEVEL || LogLevel.ERROR);
14
+
15
+ const program = new Command();
16
+
17
+ program
18
+ .name('imdone')
19
+ .description('Imdone CLI to pull from and push to Jira')
20
+ .version('1.0.0');
21
+
22
+ program
23
+ .command('pull')
24
+ .description('Pull issues from Jira')
25
+ .action(async () => {
26
+ await checkLicense();
27
+ const spinner = ora('Pulling issues from Jira...').start();
28
+
29
+ try {
30
+ const { result } = await pullFromJira(process.cwd(), { ...pullDeps, feedback: spinner });
31
+ const { existing, added } = result
32
+ const message = existing.length > 0 || added.length > 0
33
+ ? `Pulled ${existing.length} existing and ${added.length} new issues from Jira`
34
+ : 'No issues to pull from Jira';
35
+ spinner.succeed(message);
36
+ } catch (error) {
37
+ spinner.fail(`Failed to pull issues from Jira: ${error.message}`);
38
+ logger.error(error.stack);
39
+ process.exit(1);
40
+ }
41
+ });
42
+
43
+ program
44
+ .command('push')
45
+ .description('Push changes to Jira')
46
+ .action(async () => {
47
+ await checkLicense();
48
+ const spinner = ora('Pushing changes to Jira...').start();
49
+
50
+ try {
51
+ const { result } = await pushToJira(process.cwd(), { ...pushDeps, feedback: ora });
52
+ const message = result.length > 0 ? `Pushed ${result.length} changes to Jira` : 'No changes to push to Jira';
53
+ spinner.succeed(message);
54
+ result.forEach(res => {
55
+ console.log(res.url)
56
+ })
57
+ } catch (error) {
58
+ spinner.fail(`Failed to push changes to Jira: ${error.message}`);
59
+ logger.error(error.stack)
60
+ process.exit(1);
61
+ }
62
+ });
63
+
64
+ program
65
+ .command('init')
66
+ .description('Initialize Jira integration')
67
+ .option('--name [name]', 'Project name')
68
+ .option('--jira-url [jiraUrl]', 'Jira URL (e.g., https://your-domain.atlassian.net)')
69
+ .option('--jira-project-key [jiraProjectKey]', 'Jira project key (e.g., ABC)')
70
+ .option('--jira-username [jiraUsername]', 'Jira username (email)')
71
+ .option('--jira-api-token [jiraApiToken]', 'Jira API token')
72
+ .option('--jql [jql]', 'JQL query to fetch issues (e.g., Sprint in openSprints() and status != "Done")')
73
+ .action(async (options) => {
74
+ const projectPath = process.cwd();
75
+
76
+ try {
77
+ if (
78
+ !await gitRepoCheck(projectPath)
79
+ || !await gitIgnoreCheck(projectPath)
80
+ || !await dotEnvCheck(projectPath)
81
+ || !await imdoneProjectCheck(projectPath)
82
+ ) {
83
+ console.log('Configuration cancelled');
84
+ process.exit(1);
85
+ }
86
+ } catch (error) {
87
+ logger.error('Imdone initialization failed:', error);
88
+ process.exit(1);
89
+ }
90
+
91
+ const config = await promptForConfig(options);
92
+
93
+ const spinner = ora('Initializing imdone...').start();
94
+
95
+ try {
96
+ await imdoneInit(projectPath, config);
97
+ spinner.succeed(`Imdone initialization for "${config.name}" completed successfully`);
98
+ } catch (error) {
99
+ spinner.fail(`Imdone initialization failed: ${error.message}`);
100
+ logger.error(error);
101
+ process.exit(1);
102
+ }
103
+ });
104
+
105
+ program
106
+ .command('license')
107
+ .description('Enter or update your Imdone license')
108
+ .option('--token [token]', 'License token')
109
+ .option('--show', 'Show current license information')
110
+ .action(async (options) => {
111
+ const projectPath = process.cwd();
112
+
113
+ if (options.show) {
114
+ const spinner = ora('Fetching license information...').start();
115
+ try {
116
+ const licenseCheck = await verifyLicense(process.env.IMDONE_LICENSE);
117
+ if (licenseCheck.valid) {
118
+ const { name, email, expires } = licenseCheck.data
119
+ spinner.succeed(`Current license: ${name} (${email}) - Expires: ${expires.toLocaleDateString()}`);
120
+ } else {
121
+ spinner.fail(`Invalid or expired license: ${licenseCheck.reason}`);
122
+ }
123
+ } catch (error) {
124
+ spinner.fail(`Failed to fetch license information: ${error.message}`);
125
+ logger.error(error);
126
+ }
127
+ return;
128
+ }
129
+
130
+ let licenseToken = options.token;
131
+
132
+ // If no token provided via command line, prompt for it
133
+ if (!licenseToken) {
134
+ licenseToken = await promptForLicense();
135
+ }
136
+
137
+ const spinner = ora('Validating license...').start();
138
+
139
+ try {
140
+ // Verify the license is valid before storing it
141
+ const licenseCheck = await verifyLicense(licenseToken);
142
+
143
+ if (!licenseCheck.valid) {
144
+ spinner.fail(`Invalid license: ${licenseCheck.reason}`);
145
+ process.exit(1);
146
+ }
147
+
148
+ // License is valid, store it
149
+ await storeLicense(projectPath, licenseToken);
150
+
151
+ const { name, email, expires } = licenseCheck.data
152
+ spinner.succeed(`License accepted for ${name} (${email}) - Expires: ${expires.toLocaleDateString()}`);
153
+ } catch (error) {
154
+ spinner.fail(`Failed to validate or store license: ${error.message}`);
155
+ logger.error(error);
156
+ process.exit(1);
157
+ }
158
+ });
159
+
160
+ (async function main() {
161
+ await initEnv(process.cwd());
162
+ program.parse();
163
+ })();
164
+
package/src/index.js ADDED
@@ -0,0 +1,3 @@
1
+ export { pullFromJira } from './usecases/headless-pull-from-jira.js'
2
+ export { pushToJira } from './usecases/headless-push-to-jira.js'
3
+ export { imdoneInit } from './usecases/init.js'