empuno 0.0.0-stage → 1.0.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.
Files changed (4) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +77 -2
  3. package/empuno.mjs +774 -0
  4. package/package.json +30 -4
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Lutfios LLC
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,78 @@
1
- # Temporary Holding Version
1
+ # empuno CLI
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ [empuno](https://empuno.com) İK platformunun komut satırı. Yönetim panelinde
4
+ yapabildiğiniz **her işlemi** — izin onayı, bordro dönemi, vardiya planı,
5
+ zimmet, PDKS, masraf, işe alım, evrak — yapay zekâ ajanlarınıza ve
6
+ otomasyonlarınıza açar.
7
+
8
+ > Command-line client for the empuno HR platform (Turkey). Every admin-panel
9
+ > action is a command; output is always JSON. Docs: https://empuno.com/api-docs/agent
10
+
11
+ ## Kurulum
12
+
13
+ ```bash
14
+ npm i -g empuno # Node.js 18+
15
+ # ya da kurmadan:
16
+ npx empuno whoami
17
+ ```
18
+
19
+ ## Giriş
20
+
21
+ empuno panelinde **Firma Ayarları → API & Ajan → Yeni Anahtar → Tür: Ajan**
22
+ ile bir anahtar oluşturun ve modül yetkilerini seçin. Sonra:
23
+
24
+ ```bash
25
+ echo "emp_live_…" | empuno login --key-stdin # anahtar kabuk geçmişine düşmez
26
+ empuno whoami
27
+ ```
28
+
29
+ Ya da ortam değişkeniyle: `EMPUNO_API_KEY=emp_live_… empuno whoami`.
30
+
31
+ ## Kullanım
32
+
33
+ ```bash
34
+ empuno commands izin # komut ara (Türkçe başlıklarla)
35
+ empuno describe leave-requests.reject # yol parametreleri, alanlar, gereken yetki
36
+ empuno leave-requests.index --status pending
37
+ empuno leave-requests.approve 123
38
+ empuno leave-requests.reject 124 --rejection_reason "Yoğun dönem"
39
+ empuno employees.documents.store 45 --document_type is_sozlesmesi --file file=./sozlesme.pdf
40
+ empuno leave-reports.download 12 --output ./rapor.pdf
41
+ empuno call POST /manager/leave-requests/123/approve # ham çağrı
42
+ empuno api leaves --status approved # salt-okuma Company API
43
+ ```
44
+
45
+ Komutlar sunucudaki katalogdan okunur: panele yeni bir özellik geldiğinde
46
+ komutu CLI güncellemesi olmadan görünür.
47
+
48
+ - Yol parametreleri sırayla verilir; `--alan değer` bayrakları okuma
49
+ komutunda sorgu parametresi, yazma komutunda JSON gövdesi olur.
50
+ - `--alan @dosya` ya da `--file alan=dosya` → multipart yükleme.
51
+ - `--json '{…}'` / `--json @govde.json`, `--dry-run`, `--output yol`,
52
+ `--profile ad` (birden çok firma).
53
+
54
+ ## Çıktı sözleşmesi
55
+
56
+ - stdout **her zaman JSON** (TTY'de girintili).
57
+ - Okuma: `{"ok": true, "page": "…", "data": {…}}`
58
+ - Yazma: `{"ok": true, "message": "…", "redirect": {"command": "…", "params": {…}}}`
59
+ - Hata: `{"ok": false, "error": "…", "message": "…", "errors": {…}}`
60
+ - Çıkış kodu: `0` başarı, `1` API hatası, `2` kullanım hatası, `3` ağ hatası.
61
+
62
+ ## Ajanınıza verin
63
+
64
+ ```text
65
+ empuno İK sistemine `empuno` CLI ile erişimin var. Önce `empuno commands <anahtar kelime>`
66
+ ile komutu bul, `empuno describe <komut>` ile parametrelerini öğren, sonra çalıştır.
67
+ 422 → `errors` alanlarını düzeltip tekrar dene; 403 → yetki yetmiyor, kullanıcıya
68
+ `required_permission`ı söyle. Geri alınamaz işlemlerden önce kullanıcıdan onay al.
69
+ ```
70
+
71
+ ## Güvenlik
72
+
73
+ Ajan anahtarı, onu oluşturan kişinin adına ve seçilen modül yetkileriyle
74
+ çalışır; etkin yetki hiçbir zaman o kişinin yetkisini aşamaz. Her yazma işlemi
75
+ panelde anahtarın işlem günlüğünde görünür. Anahtar panelden anında iptal
76
+ edilir. Yerel yapılandırma `~/.config/empuno/config.json` (0600) dosyasındadır.
77
+
78
+ Belgeler: https://empuno.com/api-docs/agent · düz metin: https://empuno.com/api-docs/agent.txt
package/empuno.mjs ADDED
@@ -0,0 +1,774 @@
1
+ #!/usr/bin/env node
2
+ // empuno CLI — ajanlar (ve insanlar) için empuno komut satırı.
3
+ //
4
+ // Tek dosya, bağımlılıksız, Node >= 18. Komutlar sunucudaki katalogdan
5
+ // (GET /api/company/v1/catalog) okunur: panelde olan her yönetim işlemi
6
+ // burada da vardır; yeni özellikler CLI güncellemesi gerektirmeden görünür.
7
+ //
8
+ // Kurulum : npm i -g empuno (ya da kurmadan: npx empuno …)
9
+ // curl -fsSL https://empuno.com/cli/empuno.mjs -o ~/.local/bin/empuno && chmod +x ~/.local/bin/empuno
10
+ // Giriş : empuno login (anahtar: Firma Ayarları → API → Ajan anahtarı)
11
+ // Keşif : empuno commands izin → empuno describe leave-requests.approve
12
+ // Çalıştır: empuno leave-requests.approve 123
13
+ //
14
+ // Çıktı sözleşmesi: stdout HER ZAMAN JSON (TTY değilse tek satır, TTY'de
15
+ // girintili; --text ile insan okunur liste). Çıkış kodu: 0 başarı, 1 API
16
+ // hatası (4xx/5xx — gövde yine stdout'ta JSON), 2 kullanım hatası, 3 ağ hatası.
17
+ // Belgeler: https://empuno.com/api-docs/agent
18
+
19
+ import fs from 'node:fs';
20
+ import os from 'node:os';
21
+ import path from 'node:path';
22
+ import crypto from 'node:crypto';
23
+
24
+ const VERSION = '1.0.1'; // EMPUNO_CLI_VERSION — config/agent_api.php cli_version + package.json ile aynı tutulur
25
+ const NPM_PACKAGE = 'empuno';
26
+ const DEFAULT_URL = 'https://empuno.com';
27
+ const CATALOG_TTL_MS = 10 * 60 * 1000;
28
+
29
+ // ─── yardımcılar ───────────────────────────────────────────────────────────
30
+
31
+ const isTTY = process.stdout.isTTY === true;
32
+ let outputMode = null; // 'json' | 'text' | null(otomatik)
33
+ let prettyFlag = null;
34
+
35
+ class UsageError extends Error {}
36
+
37
+ function print(value) {
38
+ const pretty = prettyFlag ?? isTTY;
39
+ process.stdout.write(JSON.stringify(value, null, pretty ? 2 : 0) + '\n');
40
+ }
41
+
42
+ function wantsText() {
43
+ if (outputMode) return outputMode === 'text';
44
+ return isTTY;
45
+ }
46
+
47
+ function warn(msg) {
48
+ process.stderr.write(`empuno: ${msg}\n`);
49
+ }
50
+
51
+ function configDir() {
52
+ const base = process.env.XDG_CONFIG_HOME || path.join(os.homedir(), '.config');
53
+ return path.join(base, 'empuno');
54
+ }
55
+
56
+ function cacheDir() {
57
+ const base = process.env.XDG_CACHE_HOME || path.join(os.homedir(), '.cache');
58
+ return path.join(base, 'empuno');
59
+ }
60
+
61
+ function configPath() {
62
+ return process.env.EMPUNO_CONFIG || path.join(configDir(), 'config.json');
63
+ }
64
+
65
+ function readConfig() {
66
+ try {
67
+ return JSON.parse(fs.readFileSync(configPath(), 'utf8'));
68
+ } catch {
69
+ return { default: 'default', profiles: {} };
70
+ }
71
+ }
72
+
73
+ function writeConfig(cfg) {
74
+ const file = configPath();
75
+ fs.mkdirSync(path.dirname(file), { recursive: true, mode: 0o700 });
76
+ const tmp = `${file}.${process.pid}.tmp`;
77
+ fs.writeFileSync(tmp, JSON.stringify(cfg, null, 2) + '\n', { mode: 0o600 });
78
+ fs.renameSync(tmp, file);
79
+ }
80
+
81
+ function maskKey(key) {
82
+ return key ? `${key.slice(0, 13)}…${key.slice(-4)}` : null;
83
+ }
84
+
85
+ /** Etkin profil: --key/--url bayrağı > EMPUNO_API_KEY/EMPUNO_URL > config dosyası. */
86
+ function resolveAuth(flags) {
87
+ const cfg = readConfig();
88
+ const profileName = flags.profile || process.env.EMPUNO_PROFILE || cfg.default || 'default';
89
+ const profile = cfg.profiles?.[profileName] || {};
90
+ const key = flags.key || process.env.EMPUNO_API_KEY || profile.key || null;
91
+ const url = (flags.url || process.env.EMPUNO_URL || profile.url || DEFAULT_URL).replace(/\/+$/, '');
92
+ return { key, url, profileName };
93
+ }
94
+
95
+ function requireKey(auth) {
96
+ if (!auth.key) {
97
+ throw new UsageError('API anahtarı yok. `empuno login` çalıştırın ya da EMPUNO_API_KEY ortam değişkenini ayarlayın. Anahtar: empuno paneli → Firma Ayarları → API → "Ajan" türünde anahtar.');
98
+ }
99
+ if (!auth.key.startsWith('emp_live_')) {
100
+ throw new UsageError('Geçersiz anahtar biçimi (emp_live_ ile başlamalı).');
101
+ }
102
+ }
103
+
104
+ // ─── argüman ayrıştırma ────────────────────────────────────────────────────
105
+
106
+ const GLOBAL_FLAGS = new Set(['key', 'url', 'profile', 'pretty', 'compact', 'text', 'json-out', 'dry-run', 'output', 'refresh', 'all', 'full', 'module', 'writes', 'reads', 'help', 'key-stdin', 'query', 'json', 'file', 'quiet']);
107
+ const BOOLEAN_FLAGS = new Set(['pretty', 'compact', 'text', 'json-out', 'dry-run', 'refresh', 'all', 'full', 'writes', 'reads', 'help', 'key-stdin', 'quiet']);
108
+
109
+ /**
110
+ * argv → { positional: [], flags: {global}, fields: [[name, rawValue]] }.
111
+ * `--ad değer`, `--ad=değer`, tekrar eden bayrak → dizi, `--` sonrası hep positional.
112
+ */
113
+ function parseArgs(argv) {
114
+ const positional = [];
115
+ const flags = { query: [], file: [] };
116
+ const fields = [];
117
+ let i = 0;
118
+ let rest = false;
119
+
120
+ while (i < argv.length) {
121
+ const arg = argv[i];
122
+ if (rest || !arg.startsWith('--') || arg === '--') {
123
+ if (arg === '--' && !rest) { rest = true; i++; continue; }
124
+ positional.push(arg);
125
+ i++;
126
+ continue;
127
+ }
128
+
129
+ let name = arg.slice(2);
130
+ let value;
131
+ const eq = name.indexOf('=');
132
+ if (eq !== -1) {
133
+ value = name.slice(eq + 1);
134
+ name = name.slice(0, eq);
135
+ }
136
+
137
+ if (GLOBAL_FLAGS.has(name) && BOOLEAN_FLAGS.has(name)) {
138
+ flags[name] = value === undefined ? true : !['0', 'false', 'no'].includes(value);
139
+ i++;
140
+ continue;
141
+ }
142
+
143
+ if (value === undefined) {
144
+ const next = argv[i + 1];
145
+ if (next === undefined || (next.startsWith('--') && next.length > 2)) {
146
+ value = 'true'; // değersiz alan bayrağı → true
147
+ i++;
148
+ } else {
149
+ value = next;
150
+ i += 2;
151
+ }
152
+ } else {
153
+ i++;
154
+ }
155
+
156
+ if (name === 'query' || name === 'file') {
157
+ flags[name].push(value);
158
+ } else if (GLOBAL_FLAGS.has(name)) {
159
+ flags[name] = value;
160
+ } else {
161
+ fields.push([name, value]);
162
+ }
163
+ }
164
+
165
+ if (flags.text) outputMode = 'text';
166
+ if (flags['json-out']) outputMode = 'json';
167
+ if (flags.pretty) prettyFlag = true;
168
+ if (flags.compact) prettyFlag = false;
169
+
170
+ return { positional, flags, fields };
171
+ }
172
+
173
+ /** "true"/"12"/"[1,2]" → JSON değeri; baştaki sıfır ("0532…") ve diğerleri string kalır. */
174
+ function coerce(raw) {
175
+ if (typeof raw !== 'string') return raw;
176
+ if (raw === 'true') return true;
177
+ if (raw === 'false') return false;
178
+ if (raw === 'null') return null;
179
+ if (/^-?(0|[1-9]\d{0,14})(\.\d+)?$/.test(raw)) return Number(raw);
180
+ if ((raw.startsWith('{') && raw.endsWith('}')) || (raw.startsWith('[') && raw.endsWith(']'))) {
181
+ try { return JSON.parse(raw); } catch { /* düz metin */ }
182
+ }
183
+ return raw;
184
+ }
185
+
186
+ /** a.b.0.c=x → {a:{b:[{c:x}]}} ; tekrar eden anahtar → dizi. */
187
+ function setDeep(target, dotted, value) {
188
+ const parts = dotted.replace(/\[\]$/, '').replace(/\[(\w*)\]/g, '.$1').split('.').filter((p) => p !== '');
189
+ let node = target;
190
+ for (let i = 0; i < parts.length - 1; i++) {
191
+ const key = parts[i];
192
+ const nextIsIndex = /^\d+$/.test(parts[i + 1]);
193
+ if (node[key] === undefined || typeof node[key] !== 'object') node[key] = nextIsIndex ? [] : {};
194
+ node = node[key];
195
+ }
196
+ const last = parts[parts.length - 1];
197
+ const forceArray = dotted.endsWith('[]');
198
+ if (node[last] !== undefined && !Array.isArray(value)) {
199
+ node[last] = Array.isArray(node[last]) ? [...node[last], value] : [node[last], value];
200
+ } else {
201
+ node[last] = forceArray && !Array.isArray(value) ? [value] : value;
202
+ }
203
+ }
204
+
205
+ function readJsonArg(raw) {
206
+ const text = raw.startsWith('@') ? fs.readFileSync(raw.slice(1), 'utf8') : raw;
207
+ try {
208
+ return JSON.parse(text);
209
+ } catch (e) {
210
+ throw new UsageError(`--json geçerli JSON değil: ${e.message}`);
211
+ }
212
+ }
213
+
214
+ // ─── HTTP ──────────────────────────────────────────────────────────────────
215
+
216
+ let versionWarned = false;
217
+
218
+ function checkServerVersion(res) {
219
+ const latest = res.headers.get('x-empuno-cli-version');
220
+ if (!versionWarned && latest && compareVersions(latest, VERSION) > 0) {
221
+ versionWarned = true;
222
+ warn(`yeni CLI sürümü var (${VERSION} → ${latest}): ${installedViaNpm() ? `npm i -g ${NPM_PACKAGE}@latest` : 'empuno update'}`);
223
+ }
224
+ }
225
+
226
+ function compareVersions(a, b) {
227
+ const pa = a.split('.').map(Number);
228
+ const pb = b.split('.').map(Number);
229
+ for (let i = 0; i < 3; i++) {
230
+ if ((pa[i] || 0) !== (pb[i] || 0)) return (pa[i] || 0) - (pb[i] || 0);
231
+ }
232
+ return 0;
233
+ }
234
+
235
+ async function request(auth, method, urlPath, { query = {}, body = null, files = [], output = null, dryRun = false } = {}) {
236
+ const url = new URL(urlPath.startsWith('http') ? urlPath : auth.url + urlPath);
237
+ for (const [k, v] of Object.entries(query)) {
238
+ if (Array.isArray(v)) v.forEach((item) => url.searchParams.append(`${k}[]`, String(item)));
239
+ else if (v !== undefined && v !== null) url.searchParams.set(k, typeof v === 'object' ? JSON.stringify(v) : String(v));
240
+ }
241
+
242
+ const headers = {
243
+ Authorization: `Bearer ${auth.key}`,
244
+ Accept: 'application/json',
245
+ 'User-Agent': `empuno-cli/${VERSION} node/${process.versions.node}`,
246
+ 'X-Empuno-Cli': VERSION,
247
+ };
248
+
249
+ let sendMethod = method;
250
+ let payload;
251
+
252
+ if (files.length > 0) {
253
+ const form = new FormData();
254
+ appendForm(form, body || {});
255
+ for (const { field, file } of files) {
256
+ const buffer = fs.readFileSync(file);
257
+ form.append(field, new Blob([buffer]), path.basename(file));
258
+ }
259
+ // PHP, PUT/PATCH multipart gövdesini ayrıştırmaz → POST + _method.
260
+ if (!['GET', 'POST'].includes(method)) {
261
+ form.append('_method', method);
262
+ sendMethod = 'POST';
263
+ }
264
+ payload = form;
265
+ } else if (body && Object.keys(body).length > 0 && method !== 'GET') {
266
+ headers['Content-Type'] = 'application/json';
267
+ payload = JSON.stringify(body);
268
+ }
269
+
270
+ if (dryRun) {
271
+ return {
272
+ dryRun: true,
273
+ result: {
274
+ method: sendMethod,
275
+ url: url.toString(),
276
+ headers: { ...headers, Authorization: `Bearer ${maskKey(auth.key)}` },
277
+ body: files.length ? { fields: body, files } : body,
278
+ },
279
+ };
280
+ }
281
+
282
+ let res;
283
+ try {
284
+ res = await fetch(url, { method: sendMethod, headers, body: payload, redirect: 'manual' });
285
+ } catch (e) {
286
+ const err = new Error(`ağ hatası: ${e.cause?.message || e.message}`);
287
+ err.exitCode = 3;
288
+ throw err;
289
+ }
290
+
291
+ checkServerVersion(res);
292
+
293
+ const type = res.headers.get('content-type') || '';
294
+ let result;
295
+
296
+ if (type.includes('application/json')) {
297
+ const text = await res.text();
298
+ try { result = JSON.parse(text); } catch { result = { ok: false, error: 'invalid_json', body: text.slice(0, 2000) }; }
299
+ } else if (res.status >= 300 && res.status < 400) {
300
+ result = { ok: false, error: 'unexpected_redirect', status: res.status, location: res.headers.get('location') };
301
+ } else if (res.ok && (output || isDownload(res, type))) {
302
+ result = await saveDownload(res, type, output);
303
+ } else {
304
+ const text = await res.text();
305
+ result = { ok: res.ok, status: res.status, content_type: type, body: text.slice(0, 4000) };
306
+ }
307
+
308
+ return { status: res.status, ok: res.ok, result };
309
+ }
310
+
311
+ function appendForm(form, value, prefix = '') {
312
+ if (value === null || value === undefined) return;
313
+ if (Array.isArray(value)) {
314
+ value.forEach((item, i) => appendForm(form, item, `${prefix}[${i}]`));
315
+ } else if (typeof value === 'object') {
316
+ for (const [k, v] of Object.entries(value)) appendForm(form, v, prefix ? `${prefix}[${k}]` : k);
317
+ } else {
318
+ form.append(prefix, typeof value === 'boolean' ? (value ? '1' : '0') : String(value));
319
+ }
320
+ }
321
+
322
+ function isDownload(res, type) {
323
+ const disposition = res.headers.get('content-disposition') || '';
324
+ return disposition.includes('attachment') || /pdf|octet-stream|spreadsheet|excel|zip|image\//.test(type);
325
+ }
326
+
327
+ async function saveDownload(res, type, output) {
328
+ const disposition = res.headers.get('content-disposition') || '';
329
+ const match = /filename\*=UTF-8''([^;]+)|filename="?([^";]+)"?/i.exec(disposition);
330
+ const suggested = match ? decodeURIComponent(match[1] || match[2]) : `empuno-${Date.now()}${extensionFor(type)}`;
331
+ let target = output || suggested;
332
+ if (output && fs.existsSync(output) && fs.statSync(output).isDirectory()) target = path.join(output, path.basename(suggested));
333
+ const buffer = Buffer.from(await res.arrayBuffer());
334
+ fs.writeFileSync(target, buffer);
335
+ return { ok: true, saved: path.resolve(target), bytes: buffer.length, content_type: type };
336
+ }
337
+
338
+ function extensionFor(type) {
339
+ if (type.includes('pdf')) return '.pdf';
340
+ if (type.includes('spreadsheet') || type.includes('excel')) return '.xlsx';
341
+ if (type.includes('csv')) return '.csv';
342
+ if (type.includes('html')) return '.html';
343
+ if (type.includes('png')) return '.png';
344
+ if (type.includes('jpeg')) return '.jpg';
345
+ return '.bin';
346
+ }
347
+
348
+ // ─── katalog ───────────────────────────────────────────────────────────────
349
+
350
+ async function loadCatalog(auth, { refresh = false, all = false } = {}) {
351
+ const id = crypto.createHash('sha256').update(`${auth.url}|${auth.key}|${all}`).digest('hex').slice(0, 16);
352
+ const file = path.join(cacheDir(), `catalog-${id}.json`);
353
+
354
+ if (!refresh) {
355
+ try {
356
+ const stat = fs.statSync(file);
357
+ if (Date.now() - stat.mtimeMs < CATALOG_TTL_MS) return JSON.parse(fs.readFileSync(file, 'utf8'));
358
+ } catch { /* önbellek yok */ }
359
+ }
360
+
361
+ const { ok, result } = await request(auth, 'GET', '/api/company/v1/catalog', { query: all ? { all: 1 } : {} });
362
+ if (!ok) {
363
+ const err = new Error(result?.message || 'katalog alınamadı');
364
+ err.payload = result;
365
+ err.exitCode = 1;
366
+ throw err;
367
+ }
368
+
369
+ try {
370
+ fs.mkdirSync(path.dirname(file), { recursive: true, mode: 0o700 });
371
+ fs.writeFileSync(file, JSON.stringify(result), { mode: 0o600 });
372
+ } catch { /* önbellek yazılamazsa sorun değil */ }
373
+
374
+ return result;
375
+ }
376
+
377
+ async function findCommand(auth, name, flags) {
378
+ const catalog = await loadCatalog(auth, { refresh: flags.refresh });
379
+ let cmd = catalog.commands.find((c) => c.command === name);
380
+ if (cmd) return cmd;
381
+
382
+ if (catalog.access === 'read') {
383
+ const err = new UsageError('bu anahtar salt-okuma ("read") türünde: panel komutlarını çalıştıramaz, yalnız `empuno api …`. Firma Ayarları → API sekmesinden "Ajan" anahtarı oluşturun.');
384
+ err.payload = { ok: false, error: 'read_only_key' };
385
+ throw err;
386
+ }
387
+
388
+ // Önbellek eskiyse bir kez tazele; yoksa kapsam dışı mı, hiç yok mu söyle.
389
+ const full = await loadCatalog(auth, { refresh: true, all: true });
390
+ cmd = full.commands.find((c) => c.command === name);
391
+ if (cmd && cmd.allowed) return cmd;
392
+
393
+ if (cmd) {
394
+ const need = cmd.requires_company_admin ? ['company_admin'] : cmd.permissions;
395
+ const err = new UsageError(`"${name}" bu anahtarın kapsamında değil. Gereken yetki: ${need.join(', ') || '—'}. Anahtar kapsamını panelden genişletin (Firma Ayarları → API).`);
396
+ err.payload = { ok: false, error: 'scope_required', command: name, required: need };
397
+ throw err;
398
+ }
399
+
400
+ const suggestions = full.commands
401
+ .map((c) => c.command)
402
+ .filter((c) => c.includes(name.split('.')[0]) || name.includes(c.split('.')[0]))
403
+ .slice(0, 8);
404
+ const err = new UsageError(`bilinmeyen komut "${name}".${suggestions.length ? ` Şunlardan biri mi: ${suggestions.join(', ')}` : ' `empuno commands` ile listeleyin.'}`);
405
+ err.payload = { ok: false, error: 'unknown_command', command: name, suggestions };
406
+ throw err;
407
+ }
408
+
409
+ // ─── alt komutlar ──────────────────────────────────────────────────────────
410
+
411
+ async function cmdLogin(args) {
412
+ const { flags } = args;
413
+ let key = flags.key || process.env.EMPUNO_API_KEY || args.positional[0];
414
+
415
+ if (flags['key-stdin'] || (!key && !process.stdin.isTTY)) {
416
+ key = fs.readFileSync(0, 'utf8').trim();
417
+ } else if (!key) {
418
+ key = await prompt('API anahtarı (emp_live_…): ');
419
+ }
420
+
421
+ const url = (flags.url || process.env.EMPUNO_URL || DEFAULT_URL).replace(/\/+$/, '');
422
+ const profileName = flags.profile || 'default';
423
+ const auth = { key: key?.trim(), url, profileName };
424
+ requireKey(auth);
425
+
426
+ const { ok, result } = await request(auth, 'GET', '/api/company/v1/me');
427
+ if (!ok) {
428
+ print(result);
429
+ return 1;
430
+ }
431
+
432
+ const cfg = readConfig();
433
+ cfg.profiles = cfg.profiles || {};
434
+ cfg.profiles[profileName] = { url, key: auth.key };
435
+ if (!cfg.default || !cfg.profiles[cfg.default]) cfg.default = profileName;
436
+ writeConfig(cfg);
437
+
438
+ print({ ok: true, profile: profileName, config: configPath(), company: result.company, access: result.api_key?.access, acts_as: result.agent?.acts_as ?? null });
439
+ if (result.api_key?.access !== 'agent') {
440
+ warn('bu anahtar salt-okuma ("read") türünde: yalnız `empuno api …` uçları çalışır. Panel işlemleri için "Ajan" anahtarı oluşturun.');
441
+ }
442
+ return 0;
443
+ }
444
+
445
+ function prompt(question) {
446
+ return new Promise((resolve) => {
447
+ process.stderr.write(question);
448
+ let data = '';
449
+ process.stdin.setEncoding('utf8');
450
+ process.stdin.on('data', (chunk) => {
451
+ data += chunk;
452
+ if (data.includes('\n')) {
453
+ process.stdin.pause();
454
+ resolve(data.split('\n')[0]);
455
+ }
456
+ });
457
+ });
458
+ }
459
+
460
+ function cmdLogout(args) {
461
+ const cfg = readConfig();
462
+ const name = args.flags.profile || cfg.default || 'default';
463
+ const existed = Boolean(cfg.profiles?.[name]);
464
+ if (existed) delete cfg.profiles[name];
465
+ if (cfg.default === name) cfg.default = Object.keys(cfg.profiles || {})[0] || 'default';
466
+ writeConfig(cfg);
467
+ print({ ok: true, removed_profile: existed ? name : null, note: 'Anahtar sunucuda hâlâ geçerli; tamamen kapatmak için panelden iptal edin.' });
468
+ return 0;
469
+ }
470
+
471
+ async function cmdWhoami(args) {
472
+ const auth = resolveAuth(args.flags);
473
+ requireKey(auth);
474
+ const { ok, result } = await request(auth, 'GET', '/api/company/v1/me');
475
+ print({ ...result, cli: { version: VERSION, profile: auth.profileName, url: auth.url, key: maskKey(auth.key) } });
476
+ return ok ? 0 : 1;
477
+ }
478
+
479
+ async function cmdCommands(args) {
480
+ const { flags, positional } = args;
481
+ const auth = resolveAuth(flags);
482
+ requireKey(auth);
483
+ const catalog = await loadCatalog(auth, { refresh: flags.refresh, all: flags.all });
484
+ const needle = positional.join(' ').toLocaleLowerCase('tr');
485
+
486
+ let list = catalog.commands;
487
+ if (flags.module) list = list.filter((c) => c.module === flags.module);
488
+ if (flags.writes) list = list.filter((c) => c.kind === 'write');
489
+ if (flags.reads) list = list.filter((c) => c.kind === 'read');
490
+ if (needle) {
491
+ list = list.filter((c) => `${c.command} ${c.title || ''} ${c.summary} ${c.description || ''} ${c.module} ${c.module_label || ''}`.toLocaleLowerCase('tr').includes(needle));
492
+ }
493
+
494
+ if (wantsText()) {
495
+ let currentModule = null;
496
+ for (const c of [...list].sort((a, b) => a.module.localeCompare(b.module) || a.command.localeCompare(b.command))) {
497
+ if (c.module !== currentModule) {
498
+ currentModule = c.module;
499
+ process.stdout.write(`\n[${c.module}] ${c.module_label || ''}\n`);
500
+ }
501
+ const lock = c.allowed === false ? ' (kapsam dışı)' : '';
502
+ process.stdout.write(` ${c.command.padEnd(44)} ${c.method.padEnd(6)} ${c.title || c.summary}${lock}\n`);
503
+ }
504
+ process.stdout.write(`\n${list.length} komut. Ayrıntı: empuno describe <komut>\n`);
505
+ return 0;
506
+ }
507
+
508
+ print({
509
+ count: list.length,
510
+ commands: flags.full ? list : list.map((c) => ({ command: c.command, method: c.method, path: c.path, kind: c.kind, module: c.module, title: c.title, ...(c.summary !== c.title ? { summary: c.summary } : {}), ...(c.allowed === false ? { allowed: false } : {}) })),
511
+ hint: 'Ayrıntı (parametreler, alanlar, yetki): empuno describe <komut>',
512
+ });
513
+ return 0;
514
+ }
515
+
516
+ async function cmdDescribe(args) {
517
+ const name = args.positional[0];
518
+ if (!name) throw new UsageError('kullanım: empuno describe <komut>');
519
+ const auth = resolveAuth(args.flags);
520
+ requireKey(auth);
521
+ let cmd;
522
+ try {
523
+ cmd = await findCommand(auth, name, args.flags);
524
+ } catch (e) {
525
+ // Kapsam dışı komut da açıklanır: ajan, kullanıcıdan hangi yetkiyi isteyeceğini bilsin.
526
+ if (e.payload?.error !== 'scope_required') throw e;
527
+ const full = await loadCatalog(auth, { all: true });
528
+ cmd = full.commands.find((c) => c.command === name);
529
+ }
530
+
531
+ const example = ['empuno', cmd.command, ...cmd.path_params.map((p) => `<${p}>`)];
532
+ for (const [field, rule] of Object.entries(cmd.fields || {}).slice(0, 4)) {
533
+ if (field.includes('*')) continue;
534
+ example.push(`--${field}`, /file|image|mimes/.test(rule) ? '@./dosya.pdf' : '<değer>');
535
+ }
536
+
537
+ print({
538
+ ...cmd,
539
+ usage: example.join(' '),
540
+ notes: [
541
+ cmd.allowed === false ? `Bu anahtar bu komutu çalıştıramaz; gereken yetki: ${(cmd.requires_company_admin ? ['company_admin'] : cmd.permissions).join(', ')}.` : null,
542
+ cmd.kind === 'read' ? 'Okuma komutu: alan bayrakları sorgu parametresi olarak gider (?per_page=…).' : 'Yazma komutu: alan bayrakları JSON gövdesi olarak gider; dosya için --alan @yol.',
543
+ cmd.fields ? 'fields: sunucunun doğrulama kuralları (best-effort).' : 'fields bilinmiyor: eksik alanlar 422 yanıtının `errors` nesnesinde listelenir.',
544
+ cmd.path_params.length ? `Yol parametreleri sırayla positional verilir: ${cmd.path_params.join(', ')}.` : null,
545
+ ].filter(Boolean),
546
+ });
547
+ return 0;
548
+ }
549
+
550
+ async function cmdRun(args, name) {
551
+ const { flags, fields } = args;
552
+ const auth = resolveAuth(flags);
553
+ requireKey(auth);
554
+ const cmd = await findCommand(auth, name, flags);
555
+
556
+ // Yol parametreleri: önce positional, sonra aynı adlı bayrak.
557
+ const positional = [...args.positional];
558
+ const named = new Map();
559
+ const bodyFields = [];
560
+ for (const [k, v] of fields) {
561
+ if (cmd.path_params.includes(k)) named.set(k, v);
562
+ else bodyFields.push([k, v]);
563
+ }
564
+
565
+ let urlPath = cmd.path;
566
+ for (const param of cmd.path_params) {
567
+ const value = named.get(param) ?? positional.shift();
568
+ if (value === undefined) throw new UsageError(`eksik yol parametresi: ${param} (kullanım: empuno describe ${cmd.command})`);
569
+ urlPath = urlPath.replace(new RegExp(`\\{${param}\\??\\}`), encodeURIComponent(value));
570
+ }
571
+ if (positional.length) throw new UsageError(`fazla argüman: ${positional.join(' ')}`);
572
+
573
+ return execute(auth, cmd.method, urlPath, flags, bodyFields);
574
+ }
575
+
576
+ async function cmdCall(args) {
577
+ const [method, urlPath, ...extra] = args.positional;
578
+ if (!method || !urlPath || extra.length) throw new UsageError('kullanım: empuno call <METHOD> </manager/yol> [--alan değer …]');
579
+ const auth = resolveAuth(args.flags);
580
+ requireKey(auth);
581
+ return execute(auth, method.toUpperCase(), urlPath.startsWith('/') ? urlPath : `/${urlPath}`, args.flags, args.fields);
582
+ }
583
+
584
+ async function cmdApi(args) {
585
+ const [sub, ...extra] = args.positional;
586
+ if (!sub || extra.length) throw new UsageError('kullanım: empuno api <leaves|expenses|employees|…> [--status approved …]');
587
+ const auth = resolveAuth(args.flags);
588
+ requireKey(auth);
589
+ return execute(auth, 'GET', `/api/company/v1/${sub.replace(/^\/+/, '')}`, args.flags, args.fields);
590
+ }
591
+
592
+ async function execute(auth, method, urlPath, flags, fieldPairs) {
593
+ let body = flags.json ? readJsonArg(flags.json) : {};
594
+ const files = [];
595
+
596
+ for (const raw of flags.file) {
597
+ const eq = raw.indexOf('=');
598
+ if (eq === -1) throw new UsageError('--file alan=yol biçiminde olmalı');
599
+ files.push({ field: raw.slice(0, eq), file: raw.slice(eq + 1).replace(/^@/, '') });
600
+ }
601
+
602
+ for (const [name, raw] of fieldPairs) {
603
+ if (typeof raw === 'string' && raw.startsWith('@') && raw.length > 1 && fs.existsSync(raw.slice(1))) {
604
+ files.push({ field: name, file: raw.slice(1) });
605
+ continue;
606
+ }
607
+ setDeep(body, name, coerce(raw));
608
+ }
609
+
610
+ for (const { file } of files) {
611
+ if (!fs.existsSync(file)) throw new UsageError(`dosya bulunamadı: ${file}`);
612
+ }
613
+
614
+ const query = {};
615
+ for (const raw of flags.query) {
616
+ const eq = raw.indexOf('=');
617
+ if (eq === -1) throw new UsageError('--query anahtar=değer biçiminde olmalı');
618
+ setDeep(query, raw.slice(0, eq), coerce(raw.slice(eq + 1)));
619
+ }
620
+
621
+ // GET'te alanlar sorguya gider.
622
+ if (method === 'GET' && Object.keys(body).length) {
623
+ Object.assign(query, body);
624
+ body = {};
625
+ }
626
+
627
+ const response = await request(auth, method, urlPath, { query, body, files, output: flags.output, dryRun: flags['dry-run'] });
628
+ print(response.result);
629
+ if (response.dryRun) return 0;
630
+ return response.ok ? 0 : 1;
631
+ }
632
+
633
+ /** npm ile kurulduysa (global ya da npx önbelleği) dosya node_modules altındadır. */
634
+ function installedViaNpm() {
635
+ try {
636
+ return fs.realpathSync(process.argv[1]).split(path.sep).includes('node_modules');
637
+ } catch {
638
+ return false;
639
+ }
640
+ }
641
+
642
+ async function cmdUpdate(args) {
643
+ if (installedViaNpm()) {
644
+ // npm'in yönettiği dosyanın üzerine yazmak sürüm kaydını bozar.
645
+ print({ ok: false, error: 'managed_by_npm', version: VERSION, hint: `npm i -g ${NPM_PACKAGE}@latest` });
646
+ return 1;
647
+ }
648
+
649
+ const auth = resolveAuth(args.flags);
650
+ const target = fs.realpathSync(process.argv[1]);
651
+ const res = await fetch(`${auth.url}/cli/empuno.mjs`, { headers: { 'User-Agent': `empuno-cli/${VERSION}` } });
652
+ if (!res.ok) {
653
+ print({ ok: false, error: 'download_failed', status: res.status });
654
+ return 1;
655
+ }
656
+ const text = await res.text();
657
+ const match = /const VERSION = '([^']+)'/.exec(text);
658
+ if (!text.startsWith('#!/usr/bin/env node') || !match) {
659
+ print({ ok: false, error: 'invalid_download' });
660
+ return 1;
661
+ }
662
+ if (compareVersions(match[1], VERSION) <= 0 && !args.flags.all) {
663
+ print({ ok: true, updated: false, version: VERSION, note: 'Zaten güncel.' });
664
+ return 0;
665
+ }
666
+ const mode = fs.statSync(target).mode;
667
+ const tmp = `${target}.${process.pid}.tmp`;
668
+ fs.writeFileSync(tmp, text, { mode });
669
+ fs.renameSync(tmp, target);
670
+ print({ ok: true, updated: true, from: VERSION, to: match[1], path: target });
671
+ return 0;
672
+ }
673
+
674
+ const HELP = `empuno ${VERSION} — empuno İK sistemini komut satırından (ve ajanlardan) yönetin
675
+
676
+ KURULUM
677
+ npm i -g empuno # ya da kurmadan: npx empuno <komut>
678
+ curl -fsSL https://empuno.com/cli/empuno.mjs -o ~/.local/bin/empuno && chmod +x ~/.local/bin/empuno # npm'siz
679
+ empuno login # anahtar: panel → Firma Ayarları → API → "Ajan" anahtarı
680
+ echo "$KEY" | empuno login --key-stdin # geçmişe düşmeden
681
+
682
+ KEŞİF
683
+ empuno whoami # firma, anahtar türü, kimin adına çalıştığı
684
+ empuno commands [ara] [--module leaves] [--writes|--reads] [--all] [--full]
685
+ empuno describe <komut> # yöntem, yol, yol parametreleri, alanlar/kurallar, gereken yetki
686
+
687
+ ÇALIŞTIRMA (komut = panel rotası; listeden)
688
+ empuno leave-requests.index --status pending --per_page 50
689
+ empuno leave-requests.approve 123
690
+ empuno leave-requests.reject 123 --rejection_reason "Yoğun dönem"
691
+ empuno employees.documents.store 45 --document_type is_sozlesmesi --file file=./sozlesme.pdf
692
+ empuno leave-reports.download 12 --output ./rapor.pdf # dosya indir
693
+ empuno call POST /manager/leave-requests/123/approve # ham çağrı
694
+ empuno api leaves --status approved # salt-okuma Company API
695
+
696
+ ALANLAR
697
+ --ad değer | --ad=değer # true/false/null/sayı/JSON otomatik; baştaki sıfır string kalır
698
+ --dizi a --dizi b | --dizi[] a # dizi; --a.b.0.c x → iç içe nesne
699
+ --json '{"..."}' | --json @govde.json
700
+ --alan @./dosya.pdf | --file alan=./dosya.pdf # multipart yükleme
701
+ --query anahtar=değer # yazma isteğinde ek sorgu parametresi
702
+
703
+ GENEL BAYRAKLAR
704
+ --profile ad --key emp_live_… --url https://empuno.com --dry-run --output yol
705
+ --text | --json-out --pretty | --compact --refresh (katalog önbelleğini tazele)
706
+
707
+ ORTAM: EMPUNO_API_KEY, EMPUNO_URL, EMPUNO_PROFILE, EMPUNO_CONFIG
708
+ ÇIKTI: stdout her zaman JSON (TTY'de girintili). Çıkış kodu 0 başarı, 1 API hatası, 2 kullanım, 3 ağ.
709
+ YANIT: okuma → {ok, page, data}; yazma → {ok, message, redirect:{command, params}} veya 422 {ok:false, message, errors}
710
+ BELGE: https://empuno.com/api-docs/agent
711
+ `;
712
+
713
+ // ─── giriş noktası ─────────────────────────────────────────────────────────
714
+
715
+ async function main() {
716
+ const argv = process.argv.slice(2);
717
+ const args = parseArgs(argv);
718
+ const [sub, ...rest] = args.positional;
719
+ args.positional = rest;
720
+
721
+ if (!sub || sub === 'help' || (args.flags.help && !sub)) {
722
+ process.stdout.write(HELP);
723
+ return 0;
724
+ }
725
+ if (args.flags.help) {
726
+ args.positional = [sub];
727
+ return cmdDescribe(args);
728
+ }
729
+
730
+ switch (sub) {
731
+ case 'version':
732
+ case '--version':
733
+ print({ version: VERSION, node: process.versions.node });
734
+ return 0;
735
+ case 'login':
736
+ return cmdLogin(args);
737
+ case 'logout':
738
+ return cmdLogout(args);
739
+ case 'whoami':
740
+ case 'me':
741
+ return cmdWhoami(args);
742
+ case 'commands':
743
+ case 'ls':
744
+ return cmdCommands(args);
745
+ case 'describe':
746
+ return cmdDescribe(args);
747
+ case 'call':
748
+ return cmdCall(args);
749
+ case 'api':
750
+ return cmdApi(args);
751
+ case 'update':
752
+ return cmdUpdate(args);
753
+ case 'run':
754
+ if (!args.positional[0]) throw new UsageError('kullanım: empuno run <komut> …');
755
+ return cmdRun({ ...args, positional: args.positional.slice(1) }, args.positional[0]);
756
+ default:
757
+ return cmdRun(args, sub);
758
+ }
759
+ }
760
+
761
+ main()
762
+ .then((code) => { process.exitCode = code ?? 0; })
763
+ .catch((e) => {
764
+ if (e instanceof UsageError) {
765
+ if (e.payload) print(e.payload);
766
+ warn(e.message);
767
+ process.exitCode = 2;
768
+ return;
769
+ }
770
+ if (e.payload) print(e.payload);
771
+ else print({ ok: false, error: e.exitCode === 3 ? 'network_error' : 'cli_error', message: e.message });
772
+ warn(e.message);
773
+ process.exitCode = e.exitCode || 1;
774
+ });
package/package.json CHANGED
@@ -1,6 +1,32 @@
1
1
  {
2
2
  "name": "empuno",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "1.0.1",
4
+ "description": "empuno İK platformunun komut satırı: yönetim panelindeki tüm işlemleri (izin, bordro, vardiya, PDKS, zimmet, masraf, işe alım) yapay zekâ ajanlarına ve otomasyonlara açar.",
5
+ "type": "module",
6
+ "bin": {
7
+ "empuno": "empuno.mjs"
8
+ },
9
+ "files": [
10
+ "empuno.mjs",
11
+ "README.md",
12
+ "LICENSE"
13
+ ],
14
+ "engines": {
15
+ "node": ">=18"
16
+ },
17
+ "keywords": [
18
+ "empuno",
19
+ "cli",
20
+ "ai-agent",
21
+ "hr",
22
+ "ik",
23
+ "insan-kaynaklari",
24
+ "bordro",
25
+ "izin",
26
+ "pdks",
27
+ "turkey"
28
+ ],
29
+ "homepage": "https://empuno.com/api-docs/agent",
30
+ "author": "Lutfios LLC (https://empuno.com)",
31
+ "license": "MIT"
32
+ }