tsoft-cli 2.6.15 → 3.4.1

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,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
+