@fink-andreas/pi-linear-tools 0.2.1 → 0.4.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.
package/src/logger.js CHANGED
@@ -1,11 +1,50 @@
1
1
  /**
2
- * Structured logging module
2
+ * Structured logging module (file-first, TUI-safe)
3
3
  */
4
4
 
5
+ import { mkdirSync, appendFileSync } from 'node:fs';
6
+ import { dirname, join } from 'node:path';
7
+
5
8
  const LOG_LEVELS = ['debug', 'info', 'warn', 'error'];
6
9
  let currentLevel = process.env.LOG_LEVEL || 'info';
7
10
  let quietMode = false;
8
11
 
12
+ const LOG_TO_CONSOLE = String(process.env.PI_LINEAR_TOOLS_LOG_TO_CONSOLE || '').toLowerCase() === 'true';
13
+ const DEFAULT_LOG_FILE = process.env.PI_LINEAR_TOOLS_LOG_FILE
14
+ || join(process.env.HOME || process.cwd(), '.config', 'pi-linear-tools', 'pi-linear-tools.log');
15
+
16
+ let logFileReady = false;
17
+ let logFilePath = DEFAULT_LOG_FILE;
18
+
19
+ function ensureLogFileReady() {
20
+ if (logFileReady) return;
21
+ try {
22
+ mkdirSync(dirname(logFilePath), { recursive: true });
23
+ } catch {
24
+ // ignore; fallback handled in writeLogLine
25
+ }
26
+ logFileReady = true;
27
+ }
28
+
29
+ function writeLogLine(line, isError = false) {
30
+ try {
31
+ ensureLogFileReady();
32
+ appendFileSync(logFilePath, `${line}\n`, { encoding: 'utf8' });
33
+ } catch {
34
+ // Last-resort fallback is disabled by default to protect TUI.
35
+ // Only print when explicitly opted in.
36
+ if (LOG_TO_CONSOLE) {
37
+ if (isError) console.error(line);
38
+ else console.log(line);
39
+ }
40
+ }
41
+
42
+ if (LOG_TO_CONSOLE) {
43
+ if (isError) console.error(line);
44
+ else console.log(line);
45
+ }
46
+ }
47
+
9
48
  /**
10
49
  * Enable quiet mode (suppress info/debug/warn, keep only errors)
11
50
  */
@@ -35,7 +74,7 @@ function getTimestamp() {
35
74
  */
36
75
  function maskValue(key, value) {
37
76
  const sensitiveKeys = ['apiKey', 'token', 'password', 'secret', 'LINEAR_API_KEY'];
38
- if (sensitiveKeys.some(sk => key.toLowerCase().includes(sk.toLowerCase()))) {
77
+ if (sensitiveKeys.some((sk) => key.toLowerCase().includes(sk.toLowerCase()))) {
39
78
  return '***masked***';
40
79
  }
41
80
  return value;
@@ -50,7 +89,7 @@ function formatLog(level, message, data = {}) {
50
89
  timestamp,
51
90
  level: level.toUpperCase(),
52
91
  message,
53
- ...data
92
+ ...data,
54
93
  };
55
94
  return JSON.stringify(entry);
56
95
  }
@@ -60,7 +99,7 @@ function formatLog(level, message, data = {}) {
60
99
  */
61
100
  export function debug(message, data = {}) {
62
101
  if (shouldLog('debug')) {
63
- console.log(formatLog('debug', message, data));
102
+ writeLogLine(formatLog('debug', message, data));
64
103
  }
65
104
  }
66
105
 
@@ -69,7 +108,7 @@ export function debug(message, data = {}) {
69
108
  */
70
109
  export function info(message, data = {}) {
71
110
  if (shouldLog('info')) {
72
- console.log(formatLog('info', message, data));
111
+ writeLogLine(formatLog('info', message, data));
73
112
  }
74
113
  }
75
114
 
@@ -78,7 +117,7 @@ export function info(message, data = {}) {
78
117
  */
79
118
  export function warn(message, data = {}) {
80
119
  if (shouldLog('warn')) {
81
- console.log(formatLog('warn', message, data));
120
+ writeLogLine(formatLog('warn', message, data));
82
121
  }
83
122
  }
84
123
 
@@ -87,7 +126,7 @@ export function warn(message, data = {}) {
87
126
  */
88
127
  export function error(message, data = {}) {
89
128
  if (shouldLog('error')) {
90
- console.error(formatLog('error', message, data));
129
+ writeLogLine(formatLog('error', message, data), true);
91
130
  }
92
131
  }
93
132
 
@@ -95,13 +134,7 @@ export function error(message, data = {}) {
95
134
  * Print startup banner
96
135
  */
97
136
  export function printBanner() {
98
- const banner = `
99
- ╔════════════════════════════════════════════════════════════╗
100
- ║ pi-linear-tools ║
101
- ║ Pi extension tools for Linear SDK workflows ║
102
- ╚════════════════════════════════════════════════════════════╝
103
- `;
104
- console.log(banner);
137
+ info('pi-linear-tools startup');
105
138
  }
106
139
 
107
140
  /**
@@ -110,8 +143,8 @@ export function printBanner() {
110
143
  export function logConfig(config) {
111
144
  info('Configuration loaded', {
112
145
  ...Object.fromEntries(
113
- Object.entries(config).map(([key, value]) => [key, maskValue(key, value)])
114
- )
146
+ Object.entries(config).map(([key, value]) => [key, maskValue(key, value)]),
147
+ ),
115
148
  });
116
149
  }
117
150
 
@@ -126,3 +159,10 @@ export function setLogLevel(level) {
126
159
  warn(`Invalid log level: ${level}. Using: ${currentLevel}`);
127
160
  }
128
161
  }
162
+
163
+ /**
164
+ * Expose active log file path for diagnostics/tests
165
+ */
166
+ export function getLogFilePath() {
167
+ return logFilePath;
168
+ }
package/src/settings.js CHANGED
@@ -7,6 +7,7 @@ import { mkdir, readFile, writeFile } from 'node:fs/promises';
7
7
  import { existsSync } from 'node:fs';
8
8
  import { dirname, join } from 'node:path';
9
9
  import { debug, warn, error as logError } from './logger.js';
10
+ import { OAUTH_CLIENT_ID } from './auth/constants.js';
10
11
 
11
12
  export function getDefaultSettings() {
12
13
  return {
@@ -14,7 +15,7 @@ export function getDefaultSettings() {
14
15
  authMethod: 'api-key', // 'api-key' or 'oauth'
15
16
  apiKey: null, // Legacy API key (migrated from linearApiKey)
16
17
  oauth: {
17
- clientId: 'a3e177176c6697611367f1a2405d4a34',
18
+ clientId: OAUTH_CLIENT_ID,
18
19
  redirectUri: 'http://localhost:34711/callback',
19
20
  },
20
21
  defaultTeam: null,
@@ -49,7 +50,7 @@ function migrateSettings(settings) {
49
50
  // Add OAuth config
50
51
  if (!migrated.oauth || typeof migrated.oauth !== 'object') {
51
52
  migrated.oauth = {
52
- clientId: 'a3e177176c6697611367f1a2405d4a34',
53
+ clientId: OAUTH_CLIENT_ID,
53
54
  redirectUri: 'http://localhost:34711/callback',
54
55
  };
55
56
  }
@@ -73,14 +74,14 @@ function migrateSettings(settings) {
73
74
  // Ensure oauth config exists
74
75
  if (!migrated.oauth || typeof migrated.oauth !== 'object') {
75
76
  migrated.oauth = {
76
- clientId: 'a3e177176c6697611367f1a2405d4a34',
77
+ clientId: OAUTH_CLIENT_ID,
77
78
  redirectUri: 'http://localhost:34711/callback',
78
79
  };
79
80
  }
80
81
 
81
82
  // Ensure oauth has clientId and redirectUri
82
83
  if (migrated.oauth.clientId === undefined) {
83
- migrated.oauth.clientId = 'a3e177176c6697611367f1a2405d4a34';
84
+ migrated.oauth.clientId = OAUTH_CLIENT_ID;
84
85
  }
85
86
 
86
87
  if (migrated.oauth.redirectUri === undefined) {
package/src/shared.js ADDED
@@ -0,0 +1,112 @@
1
+ /**
2
+ * Shared utilities for pi-linear-tools entry points
3
+ *
4
+ * Common functions used by both CLI and extension entry points.
5
+ */
6
+
7
+ import fs from 'node:fs';
8
+ import path from 'node:path';
9
+ import { pathToFileURL } from 'node:url';
10
+
11
+ /**
12
+ * Check if a directory is a pi-coding-agent root
13
+ * @param {string} dir - Directory path to check
14
+ * @returns {boolean}
15
+ */
16
+ export function isPiCodingAgentRoot(dir) {
17
+ const pkgPath = path.join(dir, 'package.json');
18
+ if (!fs.existsSync(pkgPath)) return false;
19
+ try {
20
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
21
+ return pkg?.name === '@mariozechner/pi-coding-agent';
22
+ } catch {
23
+ return false;
24
+ }
25
+ }
26
+
27
+ /**
28
+ * Find the pi-coding-agent root directory
29
+ * @returns {string|null}
30
+ */
31
+ export function findPiCodingAgentRoot() {
32
+ const entry = process.argv?.[1];
33
+ if (!entry) return null;
34
+
35
+ // Method 1: walk up from argv1 (works when argv1 is .../pi-coding-agent/dist/cli.js)
36
+ {
37
+ let dir = path.dirname(entry);
38
+ for (let i = 0; i < 20; i += 1) {
39
+ if (isPiCodingAgentRoot(dir)) {
40
+ return dir;
41
+ }
42
+ const parent = path.dirname(dir);
43
+ if (parent === dir) break;
44
+ dir = parent;
45
+ }
46
+ }
47
+
48
+ // Method 2: npm global layout guess (works when argv1 is .../<prefix>/bin/pi)
49
+ // <prefix>/bin/pi -> <prefix>/lib/node_modules/@mariozechner/pi-coding-agent
50
+ {
51
+ const binDir = path.dirname(entry);
52
+ const prefix = path.resolve(binDir, '..');
53
+ const candidate = path.join(prefix, 'lib', 'node_modules', '@mariozechner', 'pi-coding-agent');
54
+ if (isPiCodingAgentRoot(candidate)) {
55
+ return candidate;
56
+ }
57
+ }
58
+
59
+ // Method 3: common global node_modules locations
60
+ for (const candidate of [
61
+ '/usr/local/lib/node_modules/@mariozechner/pi-coding-agent',
62
+ '/usr/lib/node_modules/@mariozechner/pi-coding-agent',
63
+ ]) {
64
+ if (isPiCodingAgentRoot(candidate)) {
65
+ return candidate;
66
+ }
67
+ }
68
+
69
+ return null;
70
+ }
71
+
72
+ /**
73
+ * Import from pi-coding-agent root
74
+ * @param {string} relativePathFromPiRoot - Path relative to pi root
75
+ * @returns {Promise<any>}
76
+ */
77
+ export async function importFromPiRoot(relativePathFromPiRoot) {
78
+ const piRoot = findPiCodingAgentRoot();
79
+
80
+ if (!piRoot) throw new Error('Unable to locate @mariozechner/pi-coding-agent installation');
81
+
82
+ const absPath = path.join(piRoot, relativePathFromPiRoot);
83
+ return import(pathToFileURL(absPath).href);
84
+ }
85
+
86
+ /**
87
+ * Parse command line arguments string into tokens
88
+ * @param {string} argsString - Arguments string to parse
89
+ * @returns {string[]}
90
+ */
91
+ export function parseArgs(argsString) {
92
+ if (!argsString || !argsString.trim()) return [];
93
+ const tokens = argsString.match(/"[^"]*"|'[^']*'|\S+/g) || [];
94
+ return tokens.map((t) => {
95
+ if ((t.startsWith('"') && t.endsWith('"')) || (t.startsWith("'") && t.endsWith("'"))) {
96
+ return t.slice(1, -1);
97
+ }
98
+ return t;
99
+ });
100
+ }
101
+
102
+ /**
103
+ * Read a flag value from command line arguments
104
+ * @param {string[]} args - Parsed arguments
105
+ * @param {string} flag - Flag name
106
+ * @returns {string|undefined}
107
+ */
108
+ export function readFlag(args, flag) {
109
+ const idx = args.indexOf(flag);
110
+ if (idx >= 0 && idx + 1 < args.length) return args[idx + 1];
111
+ return undefined;
112
+ }