kanban-ai 0.22.0 → 0.22.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/constants.js CHANGED
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.CACHE_SUBDIR_BINARY = exports.CACHE_DIR_NAME = exports.KANBANAI_ASSUME_NO_ENV = exports.KANBANAI_ASSUME_YES_ENV = exports.KANBANAI_NO_UPDATE_CHECK_ENV = exports.KANBANAI_GITHUB_REPO_ENV = exports.KANBANAI_BINARY_VERSION_ENV = exports.KANBANAI_HOME_ENV = exports.DEFAULT_GITHUB_REPO = void 0;
3
+ exports.CACHE_SUBDIR = exports.CONFIG_SUBDIR = exports.CACHE_SUBDIR_BINARY = exports.CACHE_DIR_NAME = exports.XDG_CACHE_HOME_ENV = exports.XDG_CONFIG_HOME_ENV = exports.KANBANAI_ASSUME_NO_ENV = exports.KANBANAI_ASSUME_YES_ENV = exports.KANBANAI_NO_UPDATE_CHECK_ENV = exports.KANBANAI_GITHUB_REPO_ENV = exports.KANBANAI_BINARY_VERSION_ENV = exports.KANBANAI_HOME_ENV = exports.DEFAULT_GITHUB_REPO = void 0;
4
4
  exports.DEFAULT_GITHUB_REPO = 'activadee/kanban-ai';
5
5
  exports.KANBANAI_HOME_ENV = 'KANBANAI_HOME';
6
6
  exports.KANBANAI_BINARY_VERSION_ENV = 'KANBANAI_BINARY_VERSION';
@@ -8,5 +8,12 @@ exports.KANBANAI_GITHUB_REPO_ENV = 'KANBANAI_GITHUB_REPO';
8
8
  exports.KANBANAI_NO_UPDATE_CHECK_ENV = 'KANBANAI_NO_UPDATE_CHECK';
9
9
  exports.KANBANAI_ASSUME_YES_ENV = 'KANBANAI_ASSUME_YES';
10
10
  exports.KANBANAI_ASSUME_NO_ENV = 'KANBANAI_ASSUME_NO';
11
+ // XDG Base Directory Specification environment variables
12
+ exports.XDG_CONFIG_HOME_ENV = 'XDG_CONFIG_HOME';
13
+ exports.XDG_CACHE_HOME_ENV = 'XDG_CACHE_HOME';
14
+ // Legacy directory name for backward compatibility
11
15
  exports.CACHE_DIR_NAME = '.kanbanAI';
12
16
  exports.CACHE_SUBDIR_BINARY = 'binary';
17
+ // New XDG-compliant subdirectory names
18
+ exports.CONFIG_SUBDIR = 'kanban-ai';
19
+ exports.CACHE_SUBDIR = 'kanban-ai';
@@ -13,5 +13,9 @@ const constants_1 = require("./constants");
13
13
  (0, vitest_1.expect)(constants_1.KANBANAI_ASSUME_NO_ENV).toBe('KANBANAI_ASSUME_NO');
14
14
  (0, vitest_1.expect)(constants_1.CACHE_DIR_NAME).toBe('.kanbanAI');
15
15
  (0, vitest_1.expect)(constants_1.CACHE_SUBDIR_BINARY).toBe('binary');
16
+ (0, vitest_1.expect)(constants_1.XDG_CONFIG_HOME_ENV).toBe('XDG_CONFIG_HOME');
17
+ (0, vitest_1.expect)(constants_1.XDG_CACHE_HOME_ENV).toBe('XDG_CACHE_HOME');
18
+ (0, vitest_1.expect)(constants_1.CONFIG_SUBDIR).toBe('kanban-ai');
19
+ (0, vitest_1.expect)(constants_1.CACHE_SUBDIR).toBe('kanban-ai');
16
20
  });
17
21
  });
package/dist/env.js CHANGED
@@ -7,10 +7,14 @@ exports.resolveEnvOptions = resolveEnvOptions;
7
7
  const node_os_1 = __importDefault(require("node:os"));
8
8
  const node_path_1 = __importDefault(require("node:path"));
9
9
  const constants_1 = require("./constants");
10
+ const paths_1 = require("./paths");
10
11
  function resolveEnvOptions() {
12
+ // Determine home directory - prefer KANBANAI_HOME for legacy compatibility
11
13
  const homeEnv = process.env[constants_1.KANBANAI_HOME_ENV] || process.env.HOME || process.env.USERPROFILE;
12
14
  const home = homeEnv || node_os_1.default.homedir();
13
- const baseCacheDir = node_path_1.default.join(home, constants_1.CACHE_DIR_NAME, constants_1.CACHE_SUBDIR_BINARY);
15
+ // Resolve XDG-compliant paths
16
+ const configDir = (0, paths_1.getConfigDir)(home);
17
+ const baseCacheDir = node_path_1.default.join((0, paths_1.getCacheDir)(home), constants_1.CACHE_SUBDIR_BINARY);
14
18
  const githubRepo = process.env[constants_1.KANBANAI_GITHUB_REPO_ENV] || constants_1.DEFAULT_GITHUB_REPO;
15
19
  const binaryVersionOverride = process.env[constants_1.KANBANAI_BINARY_VERSION_ENV] || undefined;
16
20
  const noUpdateCheck = toBoolEnv(process.env[constants_1.KANBANAI_NO_UPDATE_CHECK_ENV]);
@@ -19,6 +23,7 @@ function resolveEnvOptions() {
19
23
  return {
20
24
  githubRepo,
21
25
  baseCacheDir,
26
+ configDir,
22
27
  binaryVersionOverride,
23
28
  noUpdateCheck,
24
29
  assumeYes,
package/dist/env.test.js CHANGED
@@ -20,9 +20,12 @@ const originalEnv = { ...process.env };
20
20
  delete process.env.KANBANAI_NO_UPDATE_CHECK;
21
21
  delete process.env.KANBANAI_ASSUME_YES;
22
22
  delete process.env.KANBANAI_ASSUME_NO;
23
+ delete process.env.XDG_CONFIG_HOME;
24
+ delete process.env.XDG_CACHE_HOME;
23
25
  const env = (0, env_1.resolveEnvOptions)();
24
26
  (0, vitest_1.expect)(env.githubRepo).toBe('activadee/kanban-ai');
25
- (0, vitest_1.expect)(env.baseCacheDir.endsWith(node_path_1.default.join('.kanbanAI', 'binary'))).toBe(true);
27
+ (0, vitest_1.expect)(env.baseCacheDir.endsWith(node_path_1.default.join('.cache', 'kanban-ai', 'binary'))).toBe(true);
28
+ (0, vitest_1.expect)(env.configDir.endsWith(node_path_1.default.join('.config', 'kanban-ai'))).toBe(true);
26
29
  (0, vitest_1.expect)(env.binaryVersionOverride).toBeUndefined();
27
30
  (0, vitest_1.expect)(env.noUpdateCheck).toBe(false);
28
31
  (0, vitest_1.expect)(env.assumeYes).toBe(false);
@@ -35,12 +38,31 @@ const originalEnv = { ...process.env };
35
38
  process.env.KANBANAI_NO_UPDATE_CHECK = 'true';
36
39
  process.env.KANBANAI_ASSUME_YES = '1';
37
40
  process.env.KANBANAI_ASSUME_NO = 'yes';
41
+ process.env.XDG_CONFIG_HOME = '/custom/config';
42
+ process.env.XDG_CACHE_HOME = '/custom/cache';
38
43
  const env = (0, env_1.resolveEnvOptions)();
39
44
  (0, vitest_1.expect)(env.githubRepo).toBe('someone/else');
40
- (0, vitest_1.expect)(env.baseCacheDir).toBe(node_path_1.default.join('/tmp/kanbanai-home', '.kanbanAI', 'binary'));
45
+ (0, vitest_1.expect)(env.baseCacheDir).toBe('/custom/cache/kanban-ai/binary');
46
+ (0, vitest_1.expect)(env.configDir).toBe('/custom/config/kanban-ai');
41
47
  (0, vitest_1.expect)(env.binaryVersionOverride).toBe('1.2.3');
42
48
  (0, vitest_1.expect)(env.noUpdateCheck).toBe(true);
43
49
  (0, vitest_1.expect)(env.assumeYes).toBe(true);
44
50
  (0, vitest_1.expect)(env.assumeNo).toBe(true);
45
51
  });
52
+ (0, vitest_1.it)('uses KANBANAI_HOME for home directory when set', () => {
53
+ process.env.KANBANAI_HOME = '/custom/home';
54
+ delete process.env.XDG_CONFIG_HOME;
55
+ delete process.env.XDG_CACHE_HOME;
56
+ const env = (0, env_1.resolveEnvOptions)();
57
+ (0, vitest_1.expect)(env.baseCacheDir).toBe('/custom/home/.cache/kanban-ai/binary');
58
+ (0, vitest_1.expect)(env.configDir).toBe('/custom/home/.config/kanban-ai');
59
+ });
60
+ (0, vitest_1.it)('respects XDG environment variables', () => {
61
+ delete process.env.KANBANAI_HOME;
62
+ process.env.XDG_CONFIG_HOME = '/xdg/config';
63
+ process.env.XDG_CACHE_HOME = '/xdg/cache';
64
+ const env = (0, env_1.resolveEnvOptions)();
65
+ (0, vitest_1.expect)(env.configDir).toBe('/xdg/config/kanban-ai');
66
+ (0, vitest_1.expect)(env.baseCacheDir).toBe('/xdg/cache/kanban-ai/binary');
67
+ });
46
68
  });
package/dist/index.js CHANGED
@@ -40,7 +40,7 @@ async function runCli() {
40
40
  const platformArch = (0, platform_1.detectPlatformArch)();
41
41
  const binaryInfo = (0, platform_1.resolveBinaryInfo)(platformArch.platform, platformArch.arch);
42
42
  const githubApiCache = {
43
- dir: node_path_1.default.join(node_path_1.default.dirname(effectiveEnv.baseCacheDir), "github-api"),
43
+ dir: node_path_1.default.join(effectiveEnv.configDir, "github-api"),
44
44
  };
45
45
  const explicitVersion = cliOptions.binaryVersion
46
46
  ? (0, version_1.cleanVersionTag)(cliOptions.binaryVersion)
@@ -44,6 +44,7 @@ vitest_1.vi.mock("./env", () => ({
44
44
  resolveEnvOptions: () => ({
45
45
  githubRepo: "owner/repo",
46
46
  baseCacheDir: "/tmp/kanbanai",
47
+ configDir: "/tmp/kanbanai-config",
47
48
  binaryVersionOverride: undefined,
48
49
  noUpdateCheck: false,
49
50
  assumeYes: false,
@@ -0,0 +1,180 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.migrationNeeded = migrationNeeded;
7
+ exports.getMigrationItems = getMigrationItems;
8
+ exports.migrateFromLegacy = migrateFromLegacy;
9
+ exports.getMigrationStatus = getMigrationStatus;
10
+ const node_fs_1 = __importDefault(require("node:fs"));
11
+ const node_path_1 = __importDefault(require("node:path"));
12
+ const paths_1 = require("./paths");
13
+ /**
14
+ * Check if migration is needed (legacy directory exists)
15
+ * @returns true if legacy directory exists and new directories don't
16
+ */
17
+ function migrationNeeded() {
18
+ const legacyExists = node_fs_1.default.existsSync((0, paths_1.getLegacyDir)());
19
+ // Migration is needed if legacy exists and config doesn't
20
+ const configExists = node_fs_1.default.existsSync((0, paths_1.getConfigDir)());
21
+ return legacyExists && !configExists;
22
+ }
23
+ /**
24
+ * Get a list of files/directories that need to be migrated from legacy location
25
+ * @returns Object with configItems and cacheItems arrays
26
+ */
27
+ function getMigrationItems() {
28
+ const legacyDir = (0, paths_1.getLegacyDir)();
29
+ const configItems = [];
30
+ const cacheItems = [];
31
+ try {
32
+ const entries = node_fs_1.default.readdirSync(legacyDir, { withFileTypes: true });
33
+ for (const entry of entries) {
34
+ const fullPath = node_path_1.default.join(legacyDir, entry.name);
35
+ // GitHub API cache should go to config
36
+ if (entry.name === 'github-api' && entry.isDirectory()) {
37
+ configItems.push(entry.name);
38
+ }
39
+ // Everything else (binary, worktrees) goes to cache
40
+ else {
41
+ cacheItems.push(entry.name);
42
+ }
43
+ }
44
+ }
45
+ catch (error) {
46
+ // Directory doesn't exist or can't be read
47
+ return { configItems: [], cacheItems: [] };
48
+ }
49
+ return { configItems, cacheItems };
50
+ }
51
+ /**
52
+ * Perform the migration from legacy .kanbanAI directory to XDG-compliant locations
53
+ * @param options Options for controlling migration behavior
54
+ * @returns MigrationResult with details about the migration
55
+ */
56
+ async function migrateFromLegacy(options = {}) {
57
+ const { dryRun = false, force = false, onProgress } = options;
58
+ const result = {
59
+ success: false,
60
+ configMigrated: false,
61
+ cacheMigrated: false,
62
+ errors: [],
63
+ };
64
+ try {
65
+ const legacyDir = (0, paths_1.getLegacyDir)();
66
+ const configDir = (0, paths_1.getConfigDir)();
67
+ const cacheDir = (0, paths_1.getCacheDir)();
68
+ if (!node_fs_1.default.existsSync(legacyDir)) {
69
+ onProgress?.('No legacy directory found. Migration not needed.');
70
+ result.success = true;
71
+ return result;
72
+ }
73
+ if (node_fs_1.default.existsSync(configDir) && !force) {
74
+ onProgress?.('XDG-compliant directories already exist. Migration not needed.');
75
+ result.success = true;
76
+ return result;
77
+ }
78
+ onProgress?.(`Starting migration from ${legacyDir}`);
79
+ // Create directories if they don't exist
80
+ if (!dryRun) {
81
+ node_fs_1.default.mkdirSync(configDir, { recursive: true });
82
+ node_fs_1.default.mkdirSync(cacheDir, { recursive: true });
83
+ }
84
+ // Get items to migrate
85
+ const { configItems, cacheItems } = getMigrationItems();
86
+ if (configItems.length > 0) {
87
+ onProgress?.(`Migrating config items: ${configItems.join(', ')}`);
88
+ for (const item of configItems) {
89
+ const source = node_path_1.default.join(legacyDir, item);
90
+ const destination = node_path_1.default.join(configDir, item);
91
+ if (dryRun) {
92
+ onProgress?.(`[DRY RUN] Would copy ${source} to ${destination}`);
93
+ }
94
+ else {
95
+ onProgress?.(`Copying ${source} to ${destination}`);
96
+ await copyRecursive(source, destination);
97
+ }
98
+ }
99
+ if (!dryRun) {
100
+ result.configMigrated = true;
101
+ }
102
+ }
103
+ if (cacheItems.length > 0) {
104
+ onProgress?.(`Migrating cache items: ${cacheItems.join(', ')}`);
105
+ for (const item of cacheItems) {
106
+ const source = node_path_1.default.join(legacyDir, item);
107
+ const destination = node_path_1.default.join(cacheDir, item);
108
+ if (dryRun) {
109
+ onProgress?.(`[DRY RUN] Would copy ${source} to ${destination}`);
110
+ }
111
+ else {
112
+ onProgress?.(`Copying ${source} to ${destination}`);
113
+ await copyRecursive(source, destination);
114
+ }
115
+ }
116
+ if (!dryRun) {
117
+ result.cacheMigrated = true;
118
+ }
119
+ }
120
+ result.success = true;
121
+ onProgress?.('Migration completed successfully!');
122
+ }
123
+ catch (error) {
124
+ const errorMessage = error instanceof Error ? error.message : String(error);
125
+ result.errors.push(errorMessage);
126
+ onProgress?.(`Migration failed: ${errorMessage}`);
127
+ }
128
+ return result;
129
+ }
130
+ /**
131
+ * Copy a directory recursively
132
+ */
133
+ async function copyRecursive(source, destination) {
134
+ // Check if source is a directory
135
+ const stats = node_fs_1.default.statSync(source);
136
+ if (stats.isDirectory()) {
137
+ // Create destination directory
138
+ node_fs_1.default.mkdirSync(destination, { recursive: true });
139
+ // Copy all contents
140
+ const entries = node_fs_1.default.readdirSync(source, { withFileTypes: true });
141
+ for (const entry of entries) {
142
+ const sourcePath = node_path_1.default.join(source, entry.name);
143
+ const destPath = node_path_1.default.join(destination, entry.name);
144
+ if (entry.isDirectory()) {
145
+ await copyRecursive(sourcePath, destPath);
146
+ }
147
+ else {
148
+ node_fs_1.default.copyFileSync(sourcePath, destPath);
149
+ }
150
+ }
151
+ }
152
+ else {
153
+ // It's a file, just copy it
154
+ node_fs_1.default.copyFileSync(source, destination);
155
+ }
156
+ }
157
+ /**
158
+ * Get the status of migration without performing it
159
+ * @returns Object describing current migration status
160
+ */
161
+ function getMigrationStatus() {
162
+ const legacyDir = (0, paths_1.getLegacyDir)();
163
+ const configDir = (0, paths_1.getConfigDir)();
164
+ const cacheDir = (0, paths_1.getCacheDir)();
165
+ const legacyExists = node_fs_1.default.existsSync(legacyDir);
166
+ const configExists = node_fs_1.default.existsSync(configDir);
167
+ const cacheExists = node_fs_1.default.existsSync(cacheDir);
168
+ const needsMigration = legacyExists && (!configExists || !cacheExists);
169
+ let pendingItems = { configItems: [], cacheItems: [] };
170
+ if (needsMigration) {
171
+ pendingItems = getMigrationItems();
172
+ }
173
+ return {
174
+ legacyExists,
175
+ configExists,
176
+ cacheExists,
177
+ needsMigration,
178
+ pendingItems,
179
+ };
180
+ }
@@ -0,0 +1,107 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ const vitest_1 = require("vitest");
7
+ const node_fs_1 = __importDefault(require("node:fs"));
8
+ const node_path_1 = __importDefault(require("node:path"));
9
+ const migration_1 = require("./migration");
10
+ (0, vitest_1.describe)('Migration', () => {
11
+ const testHome = '/tmp/kanban-ai-test';
12
+ const legacyDir = node_path_1.default.join(testHome, '.kanbanAI');
13
+ const configDir = node_path_1.default.join(testHome, '.config', 'kanban-ai');
14
+ const cacheDir = node_path_1.default.join(testHome, '.cache', 'kanban-ai');
15
+ (0, vitest_1.beforeEach)(() => {
16
+ // Create test directory structure
17
+ node_fs_1.default.mkdirSync(legacyDir, { recursive: true });
18
+ node_fs_1.default.mkdirSync(node_path_1.default.join(legacyDir, 'binary'), { recursive: true });
19
+ node_fs_1.default.mkdirSync(node_path_1.default.join(legacyDir, 'github-api'), { recursive: true });
20
+ node_fs_1.default.writeFileSync(node_path_1.default.join(legacyDir, 'binary', 'test'), 'test binary');
21
+ node_fs_1.default.writeFileSync(node_path_1.default.join(legacyDir, 'github-api', 'test'), 'test cache');
22
+ });
23
+ (0, vitest_1.afterEach)(() => {
24
+ // Cleanup test directories
25
+ node_fs_1.default.rmSync(testHome, { recursive: true, force: true });
26
+ vitest_1.vi.unstubAllEnvs();
27
+ });
28
+ (0, vitest_1.describe)('migrationNeeded', () => {
29
+ (0, vitest_1.test)('returns true when legacy exists but config does not', () => {
30
+ vitest_1.vi.stubEnv('HOME', testHome);
31
+ (0, vitest_1.expect)((0, migration_1.migrationNeeded)()).toBe(true);
32
+ });
33
+ (0, vitest_1.test)('returns false when both legacy and config exist', () => {
34
+ node_fs_1.default.mkdirSync(configDir, { recursive: true });
35
+ vitest_1.vi.stubEnv('HOME', testHome);
36
+ (0, vitest_1.expect)((0, migration_1.migrationNeeded)()).toBe(false);
37
+ });
38
+ (0, vitest_1.test)('returns false when legacy does not exist', () => {
39
+ node_fs_1.default.rmSync(legacyDir, { recursive: true, force: true });
40
+ vitest_1.vi.stubEnv('HOME', testHome);
41
+ (0, vitest_1.expect)((0, migration_1.migrationNeeded)()).toBe(false);
42
+ });
43
+ });
44
+ (0, vitest_1.describe)('getMigrationItems', () => {
45
+ (0, vitest_1.test)('identifies github-api as config item', () => {
46
+ vitest_1.vi.stubEnv('HOME', testHome);
47
+ const { configItems } = (0, migration_1.getMigrationItems)();
48
+ (0, vitest_1.expect)(configItems).toContain('github-api');
49
+ });
50
+ (0, vitest_1.test)('identifies binary as cache item', () => {
51
+ vitest_1.vi.stubEnv('HOME', testHome);
52
+ const { cacheItems } = (0, migration_1.getMigrationItems)();
53
+ (0, vitest_1.expect)(cacheItems).toContain('binary');
54
+ });
55
+ });
56
+ (0, vitest_1.describe)('migrateFromLegacy', () => {
57
+ (0, vitest_1.test)('performs dry run without creating directories', async () => {
58
+ vitest_1.vi.stubEnv('HOME', testHome);
59
+ const progress = [];
60
+ const result = await (0, migration_1.migrateFromLegacy)({
61
+ dryRun: true,
62
+ onProgress: (msg) => progress.push(msg),
63
+ });
64
+ (0, vitest_1.expect)(result.success).toBe(true);
65
+ (0, vitest_1.expect)(result.configMigrated).toBe(false);
66
+ (0, vitest_1.expect)(result.cacheMigrated).toBe(false);
67
+ (0, vitest_1.expect)(progress.some(msg => msg.includes('[DRY RUN]'))).toBe(true);
68
+ });
69
+ (0, vitest_1.test)('creates XDG directories when migrating', async () => {
70
+ vitest_1.vi.stubEnv('HOME', testHome);
71
+ const result = await (0, migration_1.migrateFromLegacy)({ force: true });
72
+ (0, vitest_1.expect)(result.success).toBe(true);
73
+ (0, vitest_1.expect)(node_fs_1.default.existsSync(configDir)).toBe(true);
74
+ (0, vitest_1.expect)(node_fs_1.default.existsSync(cacheDir)).toBe(true);
75
+ });
76
+ (0, vitest_1.test)('copies github-api to config directory', async () => {
77
+ vitest_1.vi.stubEnv('HOME', testHome);
78
+ await (0, migration_1.migrateFromLegacy)({ force: true });
79
+ const githubApiPath = node_path_1.default.join(configDir, 'github-api', 'test');
80
+ (0, vitest_1.expect)(node_fs_1.default.existsSync(githubApiPath)).toBe(true);
81
+ });
82
+ (0, vitest_1.test)('copies binary to cache directory', async () => {
83
+ vitest_1.vi.stubEnv('HOME', testHome);
84
+ await (0, migration_1.migrateFromLegacy)({ force: true });
85
+ const binaryPath = node_path_1.default.join(cacheDir, 'binary', 'test');
86
+ (0, vitest_1.expect)(node_fs_1.default.existsSync(binaryPath)).toBe(true);
87
+ });
88
+ });
89
+ (0, vitest_1.describe)('getMigrationStatus', () => {
90
+ (0, vitest_1.test)('returns comprehensive status', () => {
91
+ vitest_1.vi.stubEnv('HOME', testHome);
92
+ const status = (0, migration_1.getMigrationStatus)();
93
+ (0, vitest_1.expect)(status).toHaveProperty('legacyExists');
94
+ (0, vitest_1.expect)(status).toHaveProperty('configExists');
95
+ (0, vitest_1.expect)(status).toHaveProperty('cacheExists');
96
+ (0, vitest_1.expect)(status).toHaveProperty('needsMigration');
97
+ (0, vitest_1.expect)(status).toHaveProperty('pendingItems');
98
+ });
99
+ (0, vitest_1.test)('indicates migration is needed', () => {
100
+ vitest_1.vi.stubEnv('HOME', testHome);
101
+ const status = (0, migration_1.getMigrationStatus)();
102
+ (0, vitest_1.expect)(status.legacyExists).toBe(true);
103
+ (0, vitest_1.expect)(status.configExists).toBe(false);
104
+ (0, vitest_1.expect)(status.needsMigration).toBe(true);
105
+ });
106
+ });
107
+ });
package/dist/paths.js ADDED
@@ -0,0 +1,119 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.LEGACY_DIR_NAME = exports.CACHE_SUBDIR = exports.CONFIG_SUBDIR = exports.XDG_CACHE_HOME_ENV = exports.XDG_CONFIG_HOME_ENV = void 0;
7
+ exports.getConfigDir = getConfigDir;
8
+ exports.getCacheDir = getCacheDir;
9
+ exports.getLegacyDir = getLegacyDir;
10
+ exports.legacyDirExists = legacyDirExists;
11
+ exports.getConfigSubdirPath = getConfigSubdirPath;
12
+ exports.getCacheSubdirPath = getCacheSubdirPath;
13
+ exports.getPathInfo = getPathInfo;
14
+ const node_fs_1 = __importDefault(require("node:fs"));
15
+ const node_os_1 = __importDefault(require("node:os"));
16
+ const node_path_1 = __importDefault(require("node:path"));
17
+ // XDG Base Directory Specification constants
18
+ exports.XDG_CONFIG_HOME_ENV = 'XDG_CONFIG_HOME';
19
+ exports.XDG_CACHE_HOME_ENV = 'XDG_CACHE_HOME';
20
+ // Default subdirectory names
21
+ exports.CONFIG_SUBDIR = 'kanban-ai';
22
+ exports.CACHE_SUBDIR = 'kanban-ai';
23
+ // Legacy directory name for migration detection
24
+ exports.LEGACY_DIR_NAME = '.kanbanAI';
25
+ /**
26
+ * Check if running on Windows platform
27
+ */
28
+ function isWindows() {
29
+ return process.platform === 'win32';
30
+ }
31
+ /**
32
+ * Get the user's config directory following XDG Base Directory Specification
33
+ * On Windows, uses APPDATA if XDG_CONFIG_HOME is not set
34
+ * @param customHome Optional custom home directory (useful for testing)
35
+ * @returns The config directory path
36
+ */
37
+ function getConfigDir(customHome) {
38
+ const home = customHome || node_os_1.default.homedir();
39
+ const xdgConfigHome = process.env[exports.XDG_CONFIG_HOME_ENV];
40
+ if (xdgConfigHome) {
41
+ return node_path_1.default.join(xdgConfigHome, exports.CONFIG_SUBDIR);
42
+ }
43
+ // On Windows, use APPDATA as the conventional location
44
+ if (isWindows()) {
45
+ const appData = process.env.APPDATA || node_path_1.default.join(home, 'AppData', 'Roaming');
46
+ return node_path_1.default.join(appData, exports.CONFIG_SUBDIR);
47
+ }
48
+ // On Unix/Linux, use XDG default
49
+ return node_path_1.default.join(home, '.config', exports.CONFIG_SUBDIR);
50
+ }
51
+ /**
52
+ * Get the user's cache directory following XDG Base Directory Specification
53
+ * On Windows, uses LOCALAPPDATA if XDG_CACHE_HOME is not set
54
+ * @param customHome Optional custom home directory (useful for testing)
55
+ * @returns The cache directory path
56
+ */
57
+ function getCacheDir(customHome) {
58
+ const home = customHome || node_os_1.default.homedir();
59
+ const xdgCacheHome = process.env[exports.XDG_CACHE_HOME_ENV];
60
+ if (xdgCacheHome) {
61
+ return node_path_1.default.join(xdgCacheHome, exports.CACHE_SUBDIR);
62
+ }
63
+ // On Windows, use LOCALAPPDATA as the conventional location
64
+ if (isWindows()) {
65
+ const localAppData = process.env.LOCALAPPDATA || node_path_1.default.join(home, 'AppData', 'Local');
66
+ return node_path_1.default.join(localAppData, exports.CACHE_SUBDIR);
67
+ }
68
+ // On Unix/Linux, use XDG default
69
+ return node_path_1.default.join(home, '.cache', exports.CACHE_SUBDIR);
70
+ }
71
+ /**
72
+ * Get the legacy .kanbanAI directory path for migration detection
73
+ * @param customHome Optional custom home directory (useful for testing)
74
+ * @returns The legacy directory path
75
+ */
76
+ function getLegacyDir(customHome) {
77
+ const home = customHome || node_os_1.default.homedir();
78
+ return node_path_1.default.join(home, exports.LEGACY_DIR_NAME);
79
+ }
80
+ /**
81
+ * Check if the legacy .kanbanAI directory exists
82
+ * @param customHome Optional custom home directory (useful for testing)
83
+ * @returns true if legacy directory exists
84
+ */
85
+ function legacyDirExists(customHome) {
86
+ const legacyPath = getLegacyDir(customHome);
87
+ return node_fs_1.default.existsSync(legacyPath);
88
+ }
89
+ /**
90
+ * Get the path for a specific subdirectory within the config directory
91
+ * @param subdirName The subdirectory name
92
+ * @returns The full path to the subdirectory
93
+ */
94
+ function getConfigSubdirPath(subdirName) {
95
+ return node_path_1.default.join(getConfigDir(), subdirName);
96
+ }
97
+ /**
98
+ * Get the path for a specific subdirectory within the cache directory
99
+ * @param subdirName The subdirectory name
100
+ * @returns The full path to the subdirectory
101
+ */
102
+ function getCacheSubdirPath(subdirName) {
103
+ return node_path_1.default.join(getCacheDir(), subdirName);
104
+ }
105
+ /**
106
+ * Get comprehensive path information for debugging purposes
107
+ * @returns PathInfo object with all relevant paths
108
+ */
109
+ function getPathInfo() {
110
+ return {
111
+ configDir: getConfigDir(),
112
+ cacheDir: getCacheDir(),
113
+ legacyDir: getLegacyDir(),
114
+ legacyExists: legacyDirExists(),
115
+ xdgConfigHome: process.env[exports.XDG_CONFIG_HOME_ENV],
116
+ xdgCacheHome: process.env[exports.XDG_CACHE_HOME_ENV],
117
+ platform: process.platform,
118
+ };
119
+ }
@@ -0,0 +1,154 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ const vitest_1 = require("vitest");
7
+ const node_os_1 = __importDefault(require("node:os"));
8
+ const node_path_1 = __importDefault(require("node:path"));
9
+ const paths_1 = require("./paths");
10
+ (0, vitest_1.describe)('Path Resolution', () => {
11
+ const testHome = '/test/home';
12
+ (0, vitest_1.describe)('getConfigDir', () => {
13
+ (0, vitest_1.test)('uses XDG_CONFIG_HOME when set', () => {
14
+ vitest_1.vi.stubEnv('XDG_CONFIG_HOME', '/custom/config');
15
+ (0, vitest_1.expect)((0, paths_1.getConfigDir)(testHome)).toBe('/custom/config/kanban-ai');
16
+ vitest_1.vi.unstubAllEnvs();
17
+ });
18
+ (0, vitest_1.test)('falls back to ~/.config when XDG_CONFIG_HOME not set', () => {
19
+ vitest_1.vi.stubEnv('XDG_CONFIG_HOME', undefined);
20
+ (0, vitest_1.expect)((0, paths_1.getConfigDir)(testHome)).toBe('/test/home/.config/kanban-ai');
21
+ vitest_1.vi.unstubAllEnvs();
22
+ });
23
+ (0, vitest_1.test)('uses provided home directory', () => {
24
+ vitest_1.vi.stubEnv('XDG_CONFIG_HOME', undefined);
25
+ (0, vitest_1.expect)((0, paths_1.getConfigDir)('/another/home')).toBe('/another/home/.config/kanban-ai');
26
+ vitest_1.vi.unstubAllEnvs();
27
+ });
28
+ });
29
+ (0, vitest_1.describe)('getCacheDir', () => {
30
+ (0, vitest_1.test)('uses XDG_CACHE_HOME when set', () => {
31
+ vitest_1.vi.stubEnv('XDG_CACHE_HOME', '/custom/cache');
32
+ (0, vitest_1.expect)((0, paths_1.getCacheDir)(testHome)).toBe('/custom/cache/kanban-ai');
33
+ vitest_1.vi.unstubAllEnvs();
34
+ });
35
+ (0, vitest_1.test)('falls back to ~/.cache when XDG_CACHE_HOME not set', () => {
36
+ vitest_1.vi.stubEnv('XDG_CACHE_HOME', undefined);
37
+ (0, vitest_1.expect)((0, paths_1.getCacheDir)(testHome)).toBe('/test/home/.cache/kanban-ai');
38
+ vitest_1.vi.unstubAllEnvs();
39
+ });
40
+ (0, vitest_1.test)('uses provided home directory', () => {
41
+ vitest_1.vi.stubEnv('XDG_CACHE_HOME', undefined);
42
+ (0, vitest_1.expect)((0, paths_1.getCacheDir)('/another/home')).toBe('/another/home/.cache/kanban-ai');
43
+ vitest_1.vi.unstubAllEnvs();
44
+ });
45
+ });
46
+ (0, vitest_1.describe)('getLegacyDir', () => {
47
+ (0, vitest_1.test)('returns ~/.kanbanAI', () => {
48
+ (0, vitest_1.expect)((0, paths_1.getLegacyDir)(testHome)).toBe('/test/home/.kanbanAI');
49
+ });
50
+ });
51
+ (0, vitest_1.describe)('legacyDirExists', () => {
52
+ (0, vitest_1.test)('returns false when legacy directory does not exist', () => {
53
+ vitest_1.vi.stubEnv('HOME', '/nonexistent');
54
+ (0, vitest_1.expect)((0, paths_1.legacyDirExists)()).toBe(false);
55
+ vitest_1.vi.unstubAllEnvs();
56
+ });
57
+ });
58
+ (0, vitest_1.describe)('getConfigSubdirPath', () => {
59
+ (0, vitest_1.test)('creates path for config subdirectory', () => {
60
+ vitest_1.vi.stubEnv('XDG_CONFIG_HOME', undefined);
61
+ (0, vitest_1.expect)((0, paths_1.getConfigSubdirPath)('test')).toBe(node_path_1.default.join(node_os_1.default.homedir(), '.config', 'kanban-ai', 'test'));
62
+ vitest_1.vi.unstubAllEnvs();
63
+ });
64
+ });
65
+ (0, vitest_1.describe)('getCacheSubdirPath', () => {
66
+ (0, vitest_1.test)('creates path for cache subdirectory', () => {
67
+ vitest_1.vi.stubEnv('XDG_CACHE_HOME', undefined);
68
+ (0, vitest_1.expect)((0, paths_1.getCacheSubdirPath)('test')).toBe(node_path_1.default.join(node_os_1.default.homedir(), '.cache', 'kanban-ai', 'test'));
69
+ vitest_1.vi.unstubAllEnvs();
70
+ });
71
+ });
72
+ (0, vitest_1.describe)('getPathInfo', () => {
73
+ (0, vitest_1.test)('returns comprehensive path information', () => {
74
+ vitest_1.vi.stubEnv('XDG_CONFIG_HOME', undefined);
75
+ vitest_1.vi.stubEnv('XDG_CACHE_HOME', undefined);
76
+ const info = (0, paths_1.getPathInfo)();
77
+ (0, vitest_1.expect)(info).toHaveProperty('configDir');
78
+ (0, vitest_1.expect)(info).toHaveProperty('cacheDir');
79
+ (0, vitest_1.expect)(info).toHaveProperty('legacyDir');
80
+ (0, vitest_1.expect)(info).toHaveProperty('legacyExists');
81
+ (0, vitest_1.expect)(info).toHaveProperty('xdgConfigHome');
82
+ (0, vitest_1.expect)(info).toHaveProperty('xdgCacheHome');
83
+ (0, vitest_1.expect)(info).toHaveProperty('platform');
84
+ vitest_1.vi.unstubAllEnvs();
85
+ });
86
+ (0, vitest_1.test)('includes XDG environment variables when set', () => {
87
+ vitest_1.vi.stubEnv('XDG_CONFIG_HOME', '/custom/config');
88
+ vitest_1.vi.stubEnv('XDG_CACHE_HOME', '/custom/cache');
89
+ const info = (0, paths_1.getPathInfo)();
90
+ (0, vitest_1.expect)(info.xdgConfigHome).toBe('/custom/config');
91
+ (0, vitest_1.expect)(info.xdgCacheHome).toBe('/custom/cache');
92
+ vitest_1.vi.unstubAllEnvs();
93
+ });
94
+ });
95
+ (0, vitest_1.describe)('Windows Support', () => {
96
+ (0, vitest_1.test)('uses APPDATA for config on Windows when XDG not set', () => {
97
+ vitest_1.vi.stubEnv('XDG_CONFIG_HOME', undefined);
98
+ vitest_1.vi.stubEnv('APPDATA', 'C:\\Users\\test\\AppData\\Roaming');
99
+ // Mock platform detection
100
+ const originalPlatform = process.platform;
101
+ Object.defineProperty(process, 'platform', { value: 'win32', configurable: true });
102
+ const result = (0, paths_1.getConfigDir)('C:\\Users\\test');
103
+ // On Linux, path.join normalizes to forward slashes, but the path structure is correct
104
+ (0, vitest_1.expect)(result.toLowerCase()).toContain('appdata\\roaming'.toLowerCase());
105
+ (0, vitest_1.expect)(result).toContain('kanban-ai');
106
+ // Restore platform
107
+ Object.defineProperty(process, 'platform', { value: originalPlatform, configurable: true });
108
+ vitest_1.vi.unstubAllEnvs();
109
+ });
110
+ (0, vitest_1.test)('uses LOCALAPPDATA for cache on Windows when XDG not set', () => {
111
+ vitest_1.vi.stubEnv('XDG_CACHE_HOME', undefined);
112
+ vitest_1.vi.stubEnv('LOCALAPPDATA', 'C:\\Users\\test\\AppData\\Local');
113
+ // Mock platform detection
114
+ const originalPlatform = process.platform;
115
+ Object.defineProperty(process, 'platform', { value: 'win32', configurable: true });
116
+ const result = (0, paths_1.getCacheDir)('C:\\Users\\test');
117
+ (0, vitest_1.expect)(result.toLowerCase()).toContain('appdata\\local'.toLowerCase());
118
+ (0, vitest_1.expect)(result).toContain('kanban-ai');
119
+ // Restore platform
120
+ Object.defineProperty(process, 'platform', { value: originalPlatform, configurable: true });
121
+ vitest_1.vi.unstubAllEnvs();
122
+ });
123
+ (0, vitest_1.test)('falls back to AppData\\Roaming when APPDATA not set on Windows', () => {
124
+ vitest_1.vi.stubEnv('XDG_CONFIG_HOME', undefined);
125
+ vitest_1.vi.stubEnv('APPDATA', undefined);
126
+ // Mock platform detection
127
+ const originalPlatform = process.platform;
128
+ Object.defineProperty(process, 'platform', { value: 'win32', configurable: true });
129
+ const result = (0, paths_1.getConfigDir)('C:\\Users\\test');
130
+ // On Linux, path.join normalizes to forward slashes, so check for both with/without separator
131
+ const normalizedResult = result.toLowerCase().replace(/\\/g, '/');
132
+ (0, vitest_1.expect)(normalizedResult).toContain('appdata/roaming');
133
+ (0, vitest_1.expect)(result).toContain('kanban-ai');
134
+ // Restore platform
135
+ Object.defineProperty(process, 'platform', { value: originalPlatform, configurable: true });
136
+ vitest_1.vi.unstubAllEnvs();
137
+ });
138
+ (0, vitest_1.test)('falls back to AppData\\Local when LOCALAPPDATA not set on Windows', () => {
139
+ vitest_1.vi.stubEnv('XDG_CACHE_HOME', undefined);
140
+ vitest_1.vi.stubEnv('LOCALAPPDATA', undefined);
141
+ // Mock platform detection
142
+ const originalPlatform = process.platform;
143
+ Object.defineProperty(process, 'platform', { value: 'win32', configurable: true });
144
+ const result = (0, paths_1.getCacheDir)('C:\\Users\\test');
145
+ // On Linux, path.join normalizes to forward slashes, so check for both with/without separator
146
+ const normalizedResult = result.toLowerCase().replace(/\\/g, '/');
147
+ (0, vitest_1.expect)(normalizedResult).toContain('appdata/local');
148
+ (0, vitest_1.expect)(result).toContain('kanban-ai');
149
+ // Restore platform
150
+ Object.defineProperty(process, 'platform', { value: originalPlatform, configurable: true });
151
+ vitest_1.vi.unstubAllEnvs();
152
+ });
153
+ });
154
+ });
@@ -47,6 +47,7 @@ const binaryInfo = {
47
47
  const baseEnv = {
48
48
  githubRepo: "owner/repo",
49
49
  baseCacheDir: "/tmp/kanbanai",
50
+ configDir: "/tmp/kanbanai-config",
50
51
  binaryVersionOverride: undefined,
51
52
  noUpdateCheck: false,
52
53
  assumeYes: false,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kanban-ai",
3
- "version": "0.22.0",
3
+ "version": "0.22.2",
4
4
  "description": "Thin CLI wrapper that downloads and runs the KanbanAI binary from GitHub releases.",
5
5
  "bin": {
6
6
  "kanban-ai": "dist/index.js"