tsoft-cli 2.5.15 → 3.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/CHANGELOG.md +160 -0
- package/LICENSE +21 -0
- package/README.md +86 -53
- package/locales/en.json +323 -0
- package/locales/tr.json +323 -0
- package/package.json +41 -45
- package/src/api-client.js +217 -0
- package/src/commands/login.js +94 -0
- package/src/commands/logout.js +44 -0
- package/src/commands/org-switch.js +82 -0
- package/src/commands/theme-dev.js +424 -0
- package/src/commands/theme-init.js +162 -0
- package/src/commands/theme-publish.js +12 -0
- package/src/commands/theme-push.js +197 -0
- package/src/commands/theme-section.js +317 -0
- package/src/commands/theme-submit.js +14 -0
- package/src/commands/theme.js +474 -0
- package/src/commands/whoami.js +132 -0
- package/src/config.js +19 -0
- package/src/errors/error-mapper.js +158 -0
- package/src/i18n.js +84 -0
- package/src/index.js +179 -0
- package/src/output-mode.js +45 -0
- package/src/pkce.js +34 -0
- package/src/server.js +209 -0
- package/src/storage.js +488 -0
- package/src/ui/help-renderer.js +219 -0
- package/src/ui/prompt-wrapper.js +183 -0
- package/bin/run.js +0 -5
- package/build/enums.d.ts +0 -1
- package/build/enums.js +0 -7
- package/build/enums.js.map +0 -1
- package/build/index.d.ts +0 -5
- package/build/index.js +0 -10
- package/build/index.js.map +0 -1
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Push error code -> locale key mapper
|
|
3
|
+
*/
|
|
4
|
+
import { t } from '../i18n.js';
|
|
5
|
+
|
|
6
|
+
const ERROR_MAP = {
|
|
7
|
+
'PUSH-PENDING-REVIEW': {
|
|
8
|
+
message: 'error.push.pending_review.message',
|
|
9
|
+
hint: 'error.push.pending_review.hint',
|
|
10
|
+
},
|
|
11
|
+
'PUSH-AUTH': {
|
|
12
|
+
message: 'error.push.auth.message',
|
|
13
|
+
hint: 'error.push.auth.hint',
|
|
14
|
+
},
|
|
15
|
+
'PUSH-BACKEND-ERROR': {
|
|
16
|
+
message: 'error.push.backend_error.message',
|
|
17
|
+
hint: 'error.push.backend_error.hint',
|
|
18
|
+
},
|
|
19
|
+
'PUSH-BACKEND-REJECT': {
|
|
20
|
+
message: 'error.push.backend_reject.message',
|
|
21
|
+
hint: 'error.push.backend_reject.hint',
|
|
22
|
+
},
|
|
23
|
+
'PUSH-UNEXPECTED': {
|
|
24
|
+
message: 'error.push.unexpected.message',
|
|
25
|
+
hint: 'error.push.unexpected.hint',
|
|
26
|
+
},
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const HTTP_STATUS_MAP = {
|
|
30
|
+
401: {
|
|
31
|
+
message: 'error.http.401.message',
|
|
32
|
+
hint: 'error.http.401.hint',
|
|
33
|
+
},
|
|
34
|
+
403: {
|
|
35
|
+
message: 'error.http.403.message',
|
|
36
|
+
hint: 'error.http.403.hint',
|
|
37
|
+
},
|
|
38
|
+
404: {
|
|
39
|
+
message: 'error.http.404.message',
|
|
40
|
+
hint: 'error.http.404.hint',
|
|
41
|
+
},
|
|
42
|
+
409: {
|
|
43
|
+
message: 'error.http.409.message',
|
|
44
|
+
hint: 'error.http.409.hint',
|
|
45
|
+
},
|
|
46
|
+
423: {
|
|
47
|
+
message: 'error.http.423.message',
|
|
48
|
+
hint: 'error.http.423.hint',
|
|
49
|
+
},
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Push hata kodunu mesaj ve hint'e maple
|
|
54
|
+
* @param {Error} error - Axios error veya genel Error
|
|
55
|
+
* @param {boolean} verbose - Detayli cikti
|
|
56
|
+
* @returns {{ message: string, hint: string|null, raw: object|null }}
|
|
57
|
+
*/
|
|
58
|
+
export function mapPushError(error, verbose = false) {
|
|
59
|
+
const responseData = error.response?.data;
|
|
60
|
+
const errorCode = responseData?.error_code;
|
|
61
|
+
const statusCode = error.response?.status;
|
|
62
|
+
|
|
63
|
+
// 1. error_code oncelikli
|
|
64
|
+
if (errorCode && ERROR_MAP[errorCode]) {
|
|
65
|
+
const mapped = ERROR_MAP[errorCode];
|
|
66
|
+
return {
|
|
67
|
+
message: t(mapped.message),
|
|
68
|
+
hint: t(mapped.hint),
|
|
69
|
+
raw: verbose ? responseData : null,
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// 2. Sunucu mesaji varsa (error_code bilinmiyor ama mesaj var)
|
|
74
|
+
if (responseData?.message && errorCode) {
|
|
75
|
+
return {
|
|
76
|
+
message: responseData.message,
|
|
77
|
+
hint: t('error.push.verbose_hint'),
|
|
78
|
+
raw: verbose ? responseData : null,
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// 3. HTTP status fallback
|
|
83
|
+
if (statusCode) {
|
|
84
|
+
// 5xx genel
|
|
85
|
+
if (statusCode >= 500 && !HTTP_STATUS_MAP[statusCode]) {
|
|
86
|
+
return {
|
|
87
|
+
message: t('error.http.5xx.message'),
|
|
88
|
+
hint: t('error.push.verbose_hint'),
|
|
89
|
+
raw: verbose ? responseData : null,
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (HTTP_STATUS_MAP[statusCode]) {
|
|
94
|
+
const mapped = HTTP_STATUS_MAP[statusCode];
|
|
95
|
+
return {
|
|
96
|
+
message: t(mapped.message),
|
|
97
|
+
hint: t(mapped.hint),
|
|
98
|
+
raw: verbose ? responseData : null,
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// 4. Genel fallback
|
|
104
|
+
return {
|
|
105
|
+
message: error.message || t('error.fallback.message'),
|
|
106
|
+
hint: t('error.push.verbose_hint'),
|
|
107
|
+
raw: verbose ? responseData : null,
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Komut-bagimsiz generic error mapper.
|
|
113
|
+
* Tum komutlarin catch bloklarinda kullanilir.
|
|
114
|
+
*
|
|
115
|
+
* @param {Error} error - Axios error veya genel Error
|
|
116
|
+
* @param {{ command?: string, verbose?: boolean }} options
|
|
117
|
+
* @returns {{ message: string, hint: string|null, raw: object|null }}
|
|
118
|
+
*/
|
|
119
|
+
export function mapError(error, { command = 'unknown', verbose = false } = {}) {
|
|
120
|
+
const responseData = error.response?.data;
|
|
121
|
+
const statusCode = error.response?.status;
|
|
122
|
+
|
|
123
|
+
// 1. HTTP status code — mevcut HTTP_STATUS_MAP kullan
|
|
124
|
+
if (statusCode && HTTP_STATUS_MAP[statusCode]) {
|
|
125
|
+
const mapped = HTTP_STATUS_MAP[statusCode];
|
|
126
|
+
return {
|
|
127
|
+
message: t(mapped.message),
|
|
128
|
+
hint: t(mapped.hint),
|
|
129
|
+
raw: verbose ? responseData : null,
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// 2. 5xx genel sunucu hatasi
|
|
134
|
+
if (statusCode && statusCode >= 500) {
|
|
135
|
+
return {
|
|
136
|
+
message: t('error.http.5xx.message'),
|
|
137
|
+
hint: t('error.http.5xx.hint', { command }),
|
|
138
|
+
raw: verbose ? responseData : null,
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// 3. Network/auth hatasi — error.message kontrolu
|
|
143
|
+
const msg = error.message || '';
|
|
144
|
+
if (msg.includes('Token bulunamadi') || msg.includes('RELOGIN_REQUIRED') || msg.includes('giris') || msg.includes('login')) {
|
|
145
|
+
return {
|
|
146
|
+
message: t('error.auth_required.message'),
|
|
147
|
+
hint: t('error.auth_required.hint'),
|
|
148
|
+
raw: null,
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// 4. Genel fallback — error.message passthrough if not a locale key
|
|
153
|
+
return {
|
|
154
|
+
message: msg || t('error.fallback.message'),
|
|
155
|
+
hint: null,
|
|
156
|
+
raw: verbose ? responseData : null,
|
|
157
|
+
};
|
|
158
|
+
}
|
package/src/i18n.js
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { readFileSync } from 'fs';
|
|
2
|
+
import { fileURLToPath } from 'url';
|
|
3
|
+
import { dirname, join } from 'path';
|
|
4
|
+
|
|
5
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
6
|
+
const PROJECT_ROOT = join(__dirname, '..');
|
|
7
|
+
|
|
8
|
+
let messages = {};
|
|
9
|
+
let currentLocale = 'tr';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* OS locale auto-detect: 'en' or 'tr', default 'tr'
|
|
13
|
+
*/
|
|
14
|
+
function detectOsLocale() {
|
|
15
|
+
const sources = [
|
|
16
|
+
process.env.LANG,
|
|
17
|
+
process.env.LC_ALL,
|
|
18
|
+
(() => {
|
|
19
|
+
try {
|
|
20
|
+
return Intl.DateTimeFormat().resolvedOptions().locale;
|
|
21
|
+
} catch {
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
})(),
|
|
25
|
+
];
|
|
26
|
+
|
|
27
|
+
for (const src of sources) {
|
|
28
|
+
if (!src) continue;
|
|
29
|
+
const lower = src.toLowerCase();
|
|
30
|
+
if (lower.startsWith('en')) return 'en';
|
|
31
|
+
if (lower.startsWith('tr')) return 'tr';
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return 'tr';
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Initialize locale system. Must be called once before any t() calls.
|
|
39
|
+
* Priority: TSOFT_LANG env var → default 'tr'
|
|
40
|
+
*/
|
|
41
|
+
export function initLocale() {
|
|
42
|
+
const envLang = process.env.TSOFT_LANG;
|
|
43
|
+
if (envLang === 'en' || envLang === 'tr') {
|
|
44
|
+
currentLocale = envLang;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const localePath = join(PROJECT_ROOT, 'locales', `${currentLocale}.json`);
|
|
48
|
+
try {
|
|
49
|
+
messages = JSON.parse(readFileSync(localePath, 'utf-8'));
|
|
50
|
+
} catch (err) {
|
|
51
|
+
// Fallback to TR if locale file is missing
|
|
52
|
+
if (currentLocale !== 'tr') {
|
|
53
|
+
currentLocale = 'tr';
|
|
54
|
+
const fallbackPath = join(PROJECT_ROOT, 'locales', 'tr.json');
|
|
55
|
+
messages = JSON.parse(readFileSync(fallbackPath, 'utf-8'));
|
|
56
|
+
} else {
|
|
57
|
+
messages = {};
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Translate a key. Returns the key itself as fallback if not found.
|
|
64
|
+
* Supports {{var}} placeholder interpolation.
|
|
65
|
+
*
|
|
66
|
+
* @param {string} key - Dot-notation locale key (e.g. 'error.push.auth.message')
|
|
67
|
+
* @param {Object} vars - Variables for {{var}} interpolation
|
|
68
|
+
* @returns {string}
|
|
69
|
+
*/
|
|
70
|
+
export function t(key, vars = {}) {
|
|
71
|
+
let value = messages[key];
|
|
72
|
+
|
|
73
|
+
if (value === undefined) {
|
|
74
|
+
if (process.env.TSOFT_DEBUG === '1') {
|
|
75
|
+
process.stderr.write(`[i18n] Missing key: ${key}\n`);
|
|
76
|
+
}
|
|
77
|
+
return key;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Replace {{varName}} placeholders
|
|
81
|
+
return value.replace(/\{\{(\w+)\}\}/g, (_, name) => {
|
|
82
|
+
return vars[name] !== undefined ? vars[name] : `{{${name}}}`;
|
|
83
|
+
});
|
|
84
|
+
}
|
package/src/index.js
ADDED
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { Command } from 'commander';
|
|
4
|
+
import chalk from 'chalk';
|
|
5
|
+
import { setJsonMode } from './output-mode.js';
|
|
6
|
+
import { initLocale } from './i18n.js';
|
|
7
|
+
import { renderBrandedHelp } from './ui/help-renderer.js';
|
|
8
|
+
import { loginCommand } from './commands/login.js';
|
|
9
|
+
import { logoutCommand } from './commands/logout.js';
|
|
10
|
+
import { whoamiCommand } from './commands/whoami.js';
|
|
11
|
+
import { themeListCommand, themeCreateCommand, themePullCommand, themeUseCommand } from './commands/theme.js';
|
|
12
|
+
import { themeDevCommand } from './commands/theme-dev.js';
|
|
13
|
+
import { themeSectionListCommand, themeSectionAddCommand } from './commands/theme-section.js';
|
|
14
|
+
import { themePublishCommand } from './commands/theme-publish.js';
|
|
15
|
+
import { themePushCommand } from './commands/theme-push.js';
|
|
16
|
+
import { themeSubmitCommand } from './commands/theme-submit.js';
|
|
17
|
+
import { themeInitCommand } from './commands/theme-init.js';
|
|
18
|
+
import { orgSwitchCommand } from './commands/org-switch.js';
|
|
19
|
+
|
|
20
|
+
initLocale();
|
|
21
|
+
|
|
22
|
+
const program = new Command();
|
|
23
|
+
|
|
24
|
+
program
|
|
25
|
+
.name('tsoft')
|
|
26
|
+
.description('tsoft360 theme development and management tool')
|
|
27
|
+
.version('3.4.0');
|
|
28
|
+
|
|
29
|
+
// Global --json flag for machine-readable output (CI/CD)
|
|
30
|
+
program.option('--json', 'Machine-readable JSON output for CI/CD');
|
|
31
|
+
|
|
32
|
+
// preAction hook: runs before every command
|
|
33
|
+
program.hook('preAction', (thisCommand) => {
|
|
34
|
+
const opts = thisCommand.opts();
|
|
35
|
+
if (opts.json) {
|
|
36
|
+
setJsonMode(true);
|
|
37
|
+
chalk.level = 0; // Disable chalk colors — no ANSI codes inside JSON
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
// Show branded help when no arguments provided
|
|
42
|
+
program.action(async () => {
|
|
43
|
+
await renderBrandedHelp(program);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
// Intercept root --help before Commander.parse() to handle async branded help
|
|
47
|
+
if (process.argv.length === 3 && process.argv[2] === '--help') {
|
|
48
|
+
await renderBrandedHelp(program);
|
|
49
|
+
process.exit(0);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// login command
|
|
53
|
+
program
|
|
54
|
+
.command('login')
|
|
55
|
+
.description('Authenticate with your tsoft360 store via OAuth2')
|
|
56
|
+
.action(async () => {
|
|
57
|
+
await loginCommand();
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
// logout command
|
|
61
|
+
program
|
|
62
|
+
.command('logout')
|
|
63
|
+
.description('Sign out and clear local credentials')
|
|
64
|
+
.action(async () => {
|
|
65
|
+
await logoutCommand();
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
// whoami command
|
|
69
|
+
program
|
|
70
|
+
.command('whoami')
|
|
71
|
+
.description('Show the logged-in user and store info')
|
|
72
|
+
.action(async () => {
|
|
73
|
+
await whoamiCommand();
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
const theme = program
|
|
77
|
+
.command('theme')
|
|
78
|
+
.description('Theme management commands');
|
|
79
|
+
|
|
80
|
+
theme
|
|
81
|
+
.command('list')
|
|
82
|
+
.description('List all themes in your store')
|
|
83
|
+
.action(async () => {
|
|
84
|
+
await themeListCommand();
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
theme
|
|
88
|
+
.command('create')
|
|
89
|
+
.description('Create a new theme')
|
|
90
|
+
.action(async () => {
|
|
91
|
+
await themeCreateCommand();
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
theme
|
|
95
|
+
.command('pull')
|
|
96
|
+
.description('Download an existing theme')
|
|
97
|
+
.action(async () => {
|
|
98
|
+
await themePullCommand();
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
theme
|
|
102
|
+
.command('dev [theme-name]')
|
|
103
|
+
.description('Start theme development server with live reload')
|
|
104
|
+
.action(async (themeName) => {
|
|
105
|
+
await themeDevCommand(themeName);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
theme
|
|
109
|
+
.command('use [theme-name]')
|
|
110
|
+
.description('Set the active theme')
|
|
111
|
+
.action(async (themeName) => {
|
|
112
|
+
await themeUseCommand(themeName);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
theme
|
|
116
|
+
.command('push [theme-name]')
|
|
117
|
+
.description('Push theme files to your store')
|
|
118
|
+
.option('--verbose', 'Verbose error output')
|
|
119
|
+
.action(async (themeName, options) => {
|
|
120
|
+
await themePushCommand(themeName, options);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
const publishCmd = theme
|
|
124
|
+
.command('publish [theme-name]')
|
|
125
|
+
.description('Push theme (deprecated command)')
|
|
126
|
+
.action(async (themeName) => {
|
|
127
|
+
await themePublishCommand(themeName);
|
|
128
|
+
});
|
|
129
|
+
publishCmd._hidden = true;
|
|
130
|
+
|
|
131
|
+
const submitCmd = theme
|
|
132
|
+
.command('submit [theme-name]')
|
|
133
|
+
.description('Submit for review (deprecated command)')
|
|
134
|
+
.action(async (themeName) => {
|
|
135
|
+
await themeSubmitCommand(themeName);
|
|
136
|
+
});
|
|
137
|
+
submitCmd._hidden = true;
|
|
138
|
+
|
|
139
|
+
theme
|
|
140
|
+
.command('init')
|
|
141
|
+
.description('Initialize a new theme by forking an existing one')
|
|
142
|
+
.option('--from [slug]', 'Slug or name of the theme to fork')
|
|
143
|
+
.action(async (options) => {
|
|
144
|
+
await themeInitCommand(options.from);
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
// org commands
|
|
148
|
+
const org = program
|
|
149
|
+
.command('org')
|
|
150
|
+
.description('Organization management commands');
|
|
151
|
+
|
|
152
|
+
org
|
|
153
|
+
.command('switch')
|
|
154
|
+
.description('Switch the active organization')
|
|
155
|
+
.action(async () => {
|
|
156
|
+
await orgSwitchCommand();
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
// section commands (under theme)
|
|
160
|
+
const section = theme
|
|
161
|
+
.command('section')
|
|
162
|
+
.description('Section management commands');
|
|
163
|
+
|
|
164
|
+
section
|
|
165
|
+
.command('list')
|
|
166
|
+
.description('List all sections in the active theme')
|
|
167
|
+
.action(async () => {
|
|
168
|
+
await themeSectionListCommand();
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
section
|
|
172
|
+
.command('add')
|
|
173
|
+
.description('Add a new section to the active theme')
|
|
174
|
+
.action(async () => {
|
|
175
|
+
await themeSectionAddCommand();
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
// Parse arguments
|
|
179
|
+
program.parse(process.argv);
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* JSON output mode singleton — CI/CD destegi
|
|
3
|
+
* Her komut isJsonMode() import ederek kontrol eder.
|
|
4
|
+
* Node.js module cache sayesinde tek instance garanti.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
let _jsonMode = false;
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* JSON modu aktif et (index.js preAction hook'undan cagrilir)
|
|
11
|
+
* @param {boolean} val
|
|
12
|
+
*/
|
|
13
|
+
export function setJsonMode(val) {
|
|
14
|
+
_jsonMode = Boolean(val);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* JSON modu aktif mi?
|
|
19
|
+
* @returns {boolean}
|
|
20
|
+
*/
|
|
21
|
+
export function isJsonMode() {
|
|
22
|
+
return _jsonMode;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* JSON modunda stdout'a JSON yaz.
|
|
27
|
+
* Cagri basi sadece BIR KERE cagrilmali — partial JSON'a dikkat.
|
|
28
|
+
* @param {object} data
|
|
29
|
+
*/
|
|
30
|
+
export function jsonOut(data) {
|
|
31
|
+
if (_jsonMode) {
|
|
32
|
+
process.stdout.write(JSON.stringify(data, null, 2) + '\n');
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Insan-okunabilir output fonksiyonlarini sarmala.
|
|
38
|
+
* JSON modunda bu blok hicbir sey yazmaz.
|
|
39
|
+
* @param {Function} fn — calistirilacak fonksiyon
|
|
40
|
+
*/
|
|
41
|
+
export function humanOut(fn) {
|
|
42
|
+
if (!_jsonMode) {
|
|
43
|
+
fn();
|
|
44
|
+
}
|
|
45
|
+
}
|
package/src/pkce.js
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import crypto from 'crypto';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* PKCE code verifier oluşturur (43-128 karakter arası random string)
|
|
5
|
+
* @returns {string} Base64URL encoded code verifier
|
|
6
|
+
*/
|
|
7
|
+
export function generateCodeVerifier() {
|
|
8
|
+
return base64URLEncode(crypto.randomBytes(32));
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Code verifier'dan code challenge oluşturur
|
|
13
|
+
* @param {string} verifier - PKCE code verifier
|
|
14
|
+
* @returns {string} Base64URL encoded SHA256 hash
|
|
15
|
+
*/
|
|
16
|
+
export function generateCodeChallenge(verifier) {
|
|
17
|
+
return base64URLEncode(
|
|
18
|
+
crypto.createHash('sha256').update(verifier).digest()
|
|
19
|
+
);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Buffer'ı Base64URL formatına encode eder
|
|
24
|
+
* @param {Buffer} buffer
|
|
25
|
+
* @returns {string} Base64URL encoded string
|
|
26
|
+
*/
|
|
27
|
+
function base64URLEncode(buffer) {
|
|
28
|
+
return buffer
|
|
29
|
+
.toString('base64')
|
|
30
|
+
.replace(/\+/g, '-')
|
|
31
|
+
.replace(/\//g, '_')
|
|
32
|
+
.replace(/=/g, '');
|
|
33
|
+
}
|
|
34
|
+
|
package/src/server.js
ADDED
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
import http from 'http';
|
|
2
|
+
import { URL } from 'url';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* OAuth2 callback'i yakalamak için geçici HTTP server başlatır
|
|
6
|
+
* @param {number} port - Dinlenecek port
|
|
7
|
+
* @returns {Promise<{code: string, state: string, store: string}>}
|
|
8
|
+
*/
|
|
9
|
+
export function startCallbackServer(port) {
|
|
10
|
+
return new Promise((resolve, reject) => {
|
|
11
|
+
const server = http.createServer((req, res) => {
|
|
12
|
+
const url = new URL(req.url, `http://localhost:${port}`);
|
|
13
|
+
|
|
14
|
+
if (url.pathname === '/callback') {
|
|
15
|
+
const code = url.searchParams.get('code');
|
|
16
|
+
const state = url.searchParams.get('state');
|
|
17
|
+
const store = url.searchParams.get('store');
|
|
18
|
+
const error = url.searchParams.get('error');
|
|
19
|
+
const errorDescription = url.searchParams.get('error_description');
|
|
20
|
+
|
|
21
|
+
if (error) {
|
|
22
|
+
res.writeHead(400, { 'Content-Type': 'text/html; charset=utf-8' });
|
|
23
|
+
res.end(errorHtml(error, errorDescription));
|
|
24
|
+
server.close();
|
|
25
|
+
reject(new Error(`OAuth2 Error: ${error} - ${errorDescription || 'No description'}`));
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (!code || !state || !store) {
|
|
30
|
+
res.writeHead(400, { 'Content-Type': 'text/html; charset=utf-8' });
|
|
31
|
+
res.end(errorHtml('invalid_request', 'code, state veya store parametresi eksik'));
|
|
32
|
+
server.close();
|
|
33
|
+
reject(new Error('Eksik parametreler: code, state veya store bulunamadı'));
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
|
|
38
|
+
res.end(successHtml());
|
|
39
|
+
server.close();
|
|
40
|
+
resolve({ code, state, store });
|
|
41
|
+
} else {
|
|
42
|
+
res.writeHead(404, { 'Content-Type': 'text/html; charset=utf-8' });
|
|
43
|
+
res.end('<h1>404 - Sayfa Bulunamadı</h1>');
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
server.on('error', (err) => {
|
|
48
|
+
reject(new Error(`Server başlatılamadı: ${err.message}`));
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
server.listen(port, () => {
|
|
52
|
+
console.log(`✓ Callback sunucusu başlatıldı (http://localhost:${port}/callback)`);
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function successHtml() {
|
|
58
|
+
return `
|
|
59
|
+
<!DOCTYPE html>
|
|
60
|
+
<html lang="tr">
|
|
61
|
+
<head>
|
|
62
|
+
<meta charset="UTF-8">
|
|
63
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
64
|
+
<title>Giriş Başarılı - tsoft CLI</title>
|
|
65
|
+
<style>
|
|
66
|
+
body {
|
|
67
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
|
|
68
|
+
display: flex;
|
|
69
|
+
justify-content: center;
|
|
70
|
+
align-items: center;
|
|
71
|
+
min-height: 100vh;
|
|
72
|
+
margin: 0;
|
|
73
|
+
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
74
|
+
}
|
|
75
|
+
.container {
|
|
76
|
+
background: white;
|
|
77
|
+
padding: 3rem;
|
|
78
|
+
border-radius: 1rem;
|
|
79
|
+
box-shadow: 0 20px 60px rgba(0,0,0,0.3);
|
|
80
|
+
text-align: center;
|
|
81
|
+
max-width: 400px;
|
|
82
|
+
}
|
|
83
|
+
.success-icon {
|
|
84
|
+
width: 80px;
|
|
85
|
+
height: 80px;
|
|
86
|
+
margin: 0 auto 1.5rem;
|
|
87
|
+
background: #10b981;
|
|
88
|
+
border-radius: 50%;
|
|
89
|
+
display: flex;
|
|
90
|
+
align-items: center;
|
|
91
|
+
justify-content: center;
|
|
92
|
+
font-size: 3rem;
|
|
93
|
+
}
|
|
94
|
+
h1 {
|
|
95
|
+
color: #1f2937;
|
|
96
|
+
margin: 0 0 1rem;
|
|
97
|
+
font-size: 1.875rem;
|
|
98
|
+
}
|
|
99
|
+
p {
|
|
100
|
+
color: #6b7280;
|
|
101
|
+
margin: 0;
|
|
102
|
+
font-size: 1rem;
|
|
103
|
+
line-height: 1.5;
|
|
104
|
+
}
|
|
105
|
+
.note {
|
|
106
|
+
margin-top: 2rem;
|
|
107
|
+
padding: 1rem;
|
|
108
|
+
background: #f3f4f6;
|
|
109
|
+
border-radius: 0.5rem;
|
|
110
|
+
font-size: 0.875rem;
|
|
111
|
+
color: #4b5563;
|
|
112
|
+
}
|
|
113
|
+
</style>
|
|
114
|
+
</head>
|
|
115
|
+
<body>
|
|
116
|
+
<div class="container">
|
|
117
|
+
<div class="success-icon">✓</div>
|
|
118
|
+
<h1>Giriş Başarılı!</h1>
|
|
119
|
+
<p>Yetkilendirme işlemi tamamlandı.</p>
|
|
120
|
+
<p>Artık bu pencereyi kapatabilirsiniz.</p>
|
|
121
|
+
<div class="note">
|
|
122
|
+
Terminal'e dönün ve işleme devam edin.
|
|
123
|
+
</div>
|
|
124
|
+
</div>
|
|
125
|
+
<script>
|
|
126
|
+
setTimeout(() => window.close(), 3000);
|
|
127
|
+
</script>
|
|
128
|
+
</body>
|
|
129
|
+
</html>
|
|
130
|
+
`;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function errorHtml(error, description) {
|
|
134
|
+
return `
|
|
135
|
+
<!DOCTYPE html>
|
|
136
|
+
<html lang="tr">
|
|
137
|
+
<head>
|
|
138
|
+
<meta charset="UTF-8">
|
|
139
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
140
|
+
<title>Hata - tsoft CLI</title>
|
|
141
|
+
<style>
|
|
142
|
+
body {
|
|
143
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
|
|
144
|
+
display: flex;
|
|
145
|
+
justify-content: center;
|
|
146
|
+
align-items: center;
|
|
147
|
+
min-height: 100vh;
|
|
148
|
+
margin: 0;
|
|
149
|
+
background: linear-gradient(135deg, #f97316 0%, #dc2626 100%);
|
|
150
|
+
}
|
|
151
|
+
.container {
|
|
152
|
+
background: white;
|
|
153
|
+
padding: 3rem;
|
|
154
|
+
border-radius: 1rem;
|
|
155
|
+
box-shadow: 0 20px 60px rgba(0,0,0,0.3);
|
|
156
|
+
text-align: center;
|
|
157
|
+
max-width: 400px;
|
|
158
|
+
}
|
|
159
|
+
.error-icon {
|
|
160
|
+
width: 80px;
|
|
161
|
+
height: 80px;
|
|
162
|
+
margin: 0 auto 1.5rem;
|
|
163
|
+
background: #ef4444;
|
|
164
|
+
border-radius: 50%;
|
|
165
|
+
display: flex;
|
|
166
|
+
align-items: center;
|
|
167
|
+
justify-content: center;
|
|
168
|
+
font-size: 3rem;
|
|
169
|
+
color: white;
|
|
170
|
+
}
|
|
171
|
+
h1 {
|
|
172
|
+
color: #1f2937;
|
|
173
|
+
margin: 0 0 1rem;
|
|
174
|
+
font-size: 1.875rem;
|
|
175
|
+
}
|
|
176
|
+
p {
|
|
177
|
+
color: #6b7280;
|
|
178
|
+
margin: 0.5rem 0;
|
|
179
|
+
font-size: 1rem;
|
|
180
|
+
line-height: 1.5;
|
|
181
|
+
}
|
|
182
|
+
.error-code {
|
|
183
|
+
margin-top: 1.5rem;
|
|
184
|
+
padding: 1rem;
|
|
185
|
+
background: #fee2e2;
|
|
186
|
+
border-radius: 0.5rem;
|
|
187
|
+
font-size: 0.875rem;
|
|
188
|
+
color: #991b1b;
|
|
189
|
+
font-family: monospace;
|
|
190
|
+
}
|
|
191
|
+
</style>
|
|
192
|
+
</head>
|
|
193
|
+
<body>
|
|
194
|
+
<div class="container">
|
|
195
|
+
<div class="error-icon">✗</div>
|
|
196
|
+
<h1>Yetkilendirme Hatası</h1>
|
|
197
|
+
<p>${description || 'Bir hata oluştu'}</p>
|
|
198
|
+
<div class="error-code">
|
|
199
|
+
Hata Kodu: ${error}
|
|
200
|
+
</div>
|
|
201
|
+
<p style="margin-top: 2rem; font-size: 0.875rem;">
|
|
202
|
+
Terminal'e dönün ve tekrar deneyin.
|
|
203
|
+
</p>
|
|
204
|
+
</div>
|
|
205
|
+
</body>
|
|
206
|
+
</html>
|
|
207
|
+
`;
|
|
208
|
+
}
|
|
209
|
+
|