zedev-cli 0.1.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/README.md +86 -0
- package/bin/zedev.js +74 -0
- package/package.json +40 -0
- package/src/args.js +35 -0
- package/src/collect.js +0 -0
- package/src/commands/login.js +33 -0
- package/src/commands/misc.js +73 -0
- package/src/commands/push.js +55 -0
- package/src/commands/watch.js +77 -0
- package/src/config.js +99 -0
- package/src/mcp.js +82 -0
- package/src/ui.js +19 -0
package/README.md
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# zedev — əmr sətri
|
|
2
|
+
|
|
3
|
+
Şirkət qaydalarını qovluqdan ZEDEV platformasına göndərir. Brauzer açmağa,
|
|
4
|
+
fayl seçməyə, dialoqdan keçməyə ehtiyac yoxdur:
|
|
5
|
+
|
|
6
|
+
```bash
|
|
7
|
+
zedev push --publish
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
## Quraşdırma
|
|
11
|
+
|
|
12
|
+
Node 18.17+ lazımdır. Asılılığı yoxdur.
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
npm i -g zedev-cli
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
Hələ npm-ə dərc olunmayıbsa, repo-dan:
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
npm i -g ./cli
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## İlk dəfə
|
|
25
|
+
|
|
26
|
+
1. Platformada **Standart dəstləri** → dəstin kartında **«CLI açarı»** → açarı kopyala
|
|
27
|
+
(bir dəfə göstərilir).
|
|
28
|
+
2. Terminalda:
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
zedev login --key zedev_xxxxxxxx
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Açar `~/.zedev/config.json`-a yazılır (yalnız sənin oxuya biləcəyin fayl).
|
|
35
|
+
**Repo qovluğuna heç nə yazılmır** — açar səhvən commit olunmasın deyə.
|
|
36
|
+
|
|
37
|
+
3. Qaydaların olduğu qovluqda:
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
zedev push --publish
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## Əmrlər
|
|
44
|
+
|
|
45
|
+
| Əmr | Nə edir |
|
|
46
|
+
|---|---|
|
|
47
|
+
| `zedev login --key … [--server …]` | açarı yoxlayır və saxlayır |
|
|
48
|
+
| `zedev push [qovluq]` | qovluğu **qaralamaya** göndərir |
|
|
49
|
+
| `zedev push --publish --note "…"` | göndərir və komandaya **dərc edir** |
|
|
50
|
+
| `zedev push --dry-run -v` | heç nə göndərmir, nə gedəcəyini göstərir |
|
|
51
|
+
| `zedev push --delete-missing` | qovluqda olmayan faylları qaralamadan silir |
|
|
52
|
+
| `zedev watch [qovluq]` | dəyişiklikləri izləyir, avtomatik göndərir |
|
|
53
|
+
| `zedev status` | açar hara baxır, nə dərc olunub |
|
|
54
|
+
| `zedev init` | qovluğa `.zedev.json` (hədəf) yazır |
|
|
55
|
+
| `zedev profiles` · `use` · `logout` | bir neçə server/açar |
|
|
56
|
+
|
|
57
|
+
## İki fayl, iki məsuliyyət
|
|
58
|
+
|
|
59
|
+
| Fayl | Nə saxlayır | Paylaşıla bilər? |
|
|
60
|
+
|---|---|---|
|
|
61
|
+
| `~/.zedev/config.json` | **açar** + server | ❌ heç vaxt |
|
|
62
|
+
| qovluqdakı `.zedev.json` | hansı profil, dərc etsinmi | ✅ bəli, sirr yoxdur |
|
|
63
|
+
|
|
64
|
+
CI üçün fayl lazım deyil: `ZEDEV_KEY` və `ZEDEV_SERVER` mühit dəyişənləri işləyir.
|
|
65
|
+
|
|
66
|
+
```yaml
|
|
67
|
+
- run: npx zedev-cli push --publish --note "$GITHUB_SHA"
|
|
68
|
+
env:
|
|
69
|
+
ZEDEV_KEY: ${{ secrets.ZEDEV_KEY }}
|
|
70
|
+
ZEDEV_SERVER: https://dev.zestudio.az
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## Nə göndərilmir
|
|
74
|
+
|
|
75
|
+
`node_modules`, `.git`, `dist`, `build`, `bin`, `obj`, `.vscode`, `.idea`, `coverage`
|
|
76
|
+
və OS/VCS xidmət faylları (`.DS_Store`, `.gitignore`, …). Fayl limiti **2 MB**,
|
|
77
|
+
ümumi **20 MB** — vebdəki yükləmə ilə eyni qaydalar.
|
|
78
|
+
|
|
79
|
+
## Susqun davranışlar (qəsdən)
|
|
80
|
+
|
|
81
|
+
- **Heç nə silinmir.** Qovluqdan çıxardığın fayl platformada qalır; silmək üçün
|
|
82
|
+
`--delete-missing` yazmalısan.
|
|
83
|
+
- **Dərc ayrıca qərardır.** `push` qaralamaya yazır; komanda onu yalnız `--publish`
|
|
84
|
+
(və ya platformada «Dərc et») sonra görür.
|
|
85
|
+
- **`watch` dərc etmir.** Yazı prosesində hər saxlama yeni versiya olsaydı, versiya
|
|
86
|
+
jurnalı mənasını itirərdi.
|
package/bin/zedev.js
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { readFile } from 'node:fs/promises';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
import { login } from '../src/commands/login.js';
|
|
6
|
+
import { push } from '../src/commands/push.js';
|
|
7
|
+
import { watch } from '../src/commands/watch.js';
|
|
8
|
+
import { status, init, profiles, logout, use } from '../src/commands/misc.js';
|
|
9
|
+
import { parse } from '../src/args.js';
|
|
10
|
+
import { fail, info, bold, dim, cyan } from '../src/ui.js';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* ZEDEV CLI — şirkət qaydalarını qovluqdan platformaya göndərir.
|
|
14
|
+
*
|
|
15
|
+
* Asılılıq YOXDUR (Node 18+ `fetch`): `npm i -g` bir saniyə çəkir və gələcəkdə
|
|
16
|
+
* asılılıq zəncirindən gələn təhlükəsizlik xəbərdarlığı olmur. Alət nə qədər kiçikdirsə,
|
|
17
|
+
* o qədər uzun yaşayır.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
const HELP = `
|
|
21
|
+
${bold('zedev')} — ZEDEV standartları üçün əmr sətri
|
|
22
|
+
|
|
23
|
+
${bold('Əmrlər')}
|
|
24
|
+
${cyan('login')} --key zedev_… [--server https://…] [--as ad]
|
|
25
|
+
Açarı yoxlayır və ~/.zedev/config.json-a yazır
|
|
26
|
+
${cyan('push')} [qovluq] [--publish] [--note "…"] [--delete-missing] [--dry-run] [-v]
|
|
27
|
+
Qovluğu qaralamaya göndərir; --publish komandaya dərc edir
|
|
28
|
+
${cyan('watch')} [qovluq] [--publish]
|
|
29
|
+
Dəyişiklikləri izləyir və avtomatik göndərir
|
|
30
|
+
${cyan('status')} [qovluq]
|
|
31
|
+
Açar hara baxır, nə dərc olunub, qovluqda nə var
|
|
32
|
+
${cyan('init')} [qovluq] [--publish] [--force]
|
|
33
|
+
Qovluğa .zedev.json (hədəf) yazır — açar YOX, sirr daşımır
|
|
34
|
+
${cyan('profiles')} · ${cyan('use')} <ad> · ${cyan('logout')} [ad]
|
|
35
|
+
Bir neçə server/açar arasında keçid
|
|
36
|
+
|
|
37
|
+
${bold('Ümumi bayraqlar')}
|
|
38
|
+
--key, --server, --profile konfiqi bir dəfəlik əvəz edir
|
|
39
|
+
ZEDEV_KEY, ZEDEV_SERVER, ZEDEV_PROFILE mühit dəyişənləri (CI üçün)
|
|
40
|
+
|
|
41
|
+
${dim('Açarı platformada: Standart dəstləri → dəst → «CLI açarı».')}
|
|
42
|
+
`;
|
|
43
|
+
|
|
44
|
+
const version = async () => {
|
|
45
|
+
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
46
|
+
const pkg = JSON.parse(await readFile(path.join(here, '..', 'package.json'), 'utf8'));
|
|
47
|
+
return pkg.version;
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const [, , cmd, ...rest] = process.argv;
|
|
51
|
+
const { args, flags } = parse(rest);
|
|
52
|
+
|
|
53
|
+
try {
|
|
54
|
+
if (!cmd || flags.help || cmd === 'help' || cmd === '--help') { info(HELP); process.exit(0); }
|
|
55
|
+
if (cmd === '--version' || cmd === 'version') { info(await version()); process.exit(0); }
|
|
56
|
+
|
|
57
|
+
switch (cmd) {
|
|
58
|
+
case 'login': await login(flags); break;
|
|
59
|
+
case 'push': await push(args, flags); break;
|
|
60
|
+
case 'watch': await watch(args, flags); break;
|
|
61
|
+
case 'status': await status(args, flags); break;
|
|
62
|
+
case 'init': await init(args, flags); break;
|
|
63
|
+
case 'profiles': await profiles(); break;
|
|
64
|
+
case 'use': await use(args); break;
|
|
65
|
+
case 'logout': await logout(args); break;
|
|
66
|
+
default:
|
|
67
|
+
fail(`Naməlum əmr: ${cmd}`);
|
|
68
|
+
info(HELP);
|
|
69
|
+
process.exit(2);
|
|
70
|
+
}
|
|
71
|
+
} catch (e) {
|
|
72
|
+
fail(e.message);
|
|
73
|
+
process.exit(1);
|
|
74
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "zedev-cli",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "ZEDEV — şirkət qaydalarını qovluqdan bir əmrlə platformaya göndər",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"zedev": "bin/zedev.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"bin",
|
|
11
|
+
"src",
|
|
12
|
+
"README.md"
|
|
13
|
+
],
|
|
14
|
+
"engines": {
|
|
15
|
+
"node": ">=18.17"
|
|
16
|
+
},
|
|
17
|
+
"scripts": {
|
|
18
|
+
"test": "node --test \"test/**/*.test.mjs\"",
|
|
19
|
+
"prepublishOnly": "npm test"
|
|
20
|
+
},
|
|
21
|
+
"publishConfig": {
|
|
22
|
+
"access": "public"
|
|
23
|
+
},
|
|
24
|
+
"keywords": [
|
|
25
|
+
"zedev",
|
|
26
|
+
"standards",
|
|
27
|
+
"cli"
|
|
28
|
+
],
|
|
29
|
+
"license": "UNLICENSED",
|
|
30
|
+
"author": "ZeStudio",
|
|
31
|
+
"repository": {
|
|
32
|
+
"type": "git",
|
|
33
|
+
"url": "git+https://github.com/idrismikayil/zdev.git",
|
|
34
|
+
"directory": "cli"
|
|
35
|
+
},
|
|
36
|
+
"homepage": "https://dev.zestudio.az",
|
|
37
|
+
"bugs": {
|
|
38
|
+
"url": "https://github.com/idrismikayil/zdev/issues"
|
|
39
|
+
}
|
|
40
|
+
}
|
package/src/args.js
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Əmr sətri ayırıcısı — asılılıqsız (yargs/commander gətirmirik, alət kiçikdir).
|
|
3
|
+
*
|
|
4
|
+
* Dəstəklənən formalar: `--flag`, `--key dəyər`, `--key=dəyər`, `-v`, `-h`, sərbəst arqument.
|
|
5
|
+
*
|
|
6
|
+
* ⚠️ **Tapılmış qüsur (2026-09-14):** əvvəl dəyər yoxlaması yalnız `--`-a baxırdı, ona görə
|
|
7
|
+
* `push --dry-run -v` yazanda `-v` **`--dry-run`-ın dəyəri kimi udulurdu** və verbose heç vaxt
|
|
8
|
+
* işə düşmürdü — məhsul sahibi 54 faylı siyahı gözləyib görmədi. İndi **istənilən** `-` ilə
|
|
9
|
+
* başlayan növbəti token dəyər sayılmır.
|
|
10
|
+
*
|
|
11
|
+
* Mənfi rəqəm dəyəri (`--limit -5`) bu qaydaya qurban gedir, amma belə bayrağımız yoxdur;
|
|
12
|
+
* lazım olanda `--limit=-5` forması işləyir.
|
|
13
|
+
*/
|
|
14
|
+
export function parse(argv) {
|
|
15
|
+
const args = [];
|
|
16
|
+
const flags = {};
|
|
17
|
+
|
|
18
|
+
for (let i = 0; i < argv.length; i++) {
|
|
19
|
+
const a = argv[i];
|
|
20
|
+
|
|
21
|
+
if (a === '-v') { flags.verbose = true; continue; }
|
|
22
|
+
if (a === '-h') { flags.help = true; continue; }
|
|
23
|
+
if (!a.startsWith('--')) { args.push(a); continue; }
|
|
24
|
+
|
|
25
|
+
const eq = a.indexOf('=');
|
|
26
|
+
if (eq > 0) { flags[a.slice(2, eq)] = a.slice(eq + 1); continue; }
|
|
27
|
+
|
|
28
|
+
const name = a.slice(2);
|
|
29
|
+
const next = argv[i + 1];
|
|
30
|
+
if (next && !next.startsWith('-')) { flags[name] = next; i++; }
|
|
31
|
+
else flags[name] = true;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return { args, flags };
|
|
35
|
+
}
|
package/src/collect.js
ADDED
|
Binary file
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { Mcp } from '../mcp.js';
|
|
2
|
+
import { readGlobal, writeGlobal, normalizeServer, profileNameFor, GLOBAL_FILE } from '../config.js';
|
|
3
|
+
import { ok, info, dim, maskKey } from '../ui.js';
|
|
4
|
+
|
|
5
|
+
const DEFAULT_SERVER = 'https://dev.zestudio.az';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* `zedev login --key zedev_… [--server …] [--as ad]`
|
|
9
|
+
*
|
|
10
|
+
* Açar YOXLANIR və yalnız işlədiyi təsdiqlənəndən sonra yazılır: səhv yapışdırılmış
|
|
11
|
+
* açar diskdə qalsa, xəta növbəti əmrdə — başqa kontekstdə — çıxır və izahı çətinləşir.
|
|
12
|
+
*/
|
|
13
|
+
export async function login(flags) {
|
|
14
|
+
const key = flags.key;
|
|
15
|
+
if (!key) throw new Error('`--key zedev_…` lazımdır. Açarı platformada dəstin «CLI açarı» düyməsindən alırsan.');
|
|
16
|
+
if (!key.startsWith('zedev_')) throw new Error('Açar `zedev_` ilə başlamalıdır — yəqin natamam kopyalanıb.');
|
|
17
|
+
|
|
18
|
+
const server = normalizeServer(flags.server || process.env.ZEDEV_SERVER || DEFAULT_SERVER);
|
|
19
|
+
const name = flags.as || profileNameFor(server);
|
|
20
|
+
|
|
21
|
+
info(`${dim('Yoxlanılır:')} ${server}`);
|
|
22
|
+
const who = await new Mcp(server, key).call('project_info', {});
|
|
23
|
+
|
|
24
|
+
const cfg = await readGlobal();
|
|
25
|
+
cfg.profiles[name] = { server, key };
|
|
26
|
+
cfg.current = name;
|
|
27
|
+
await writeGlobal(cfg);
|
|
28
|
+
|
|
29
|
+
ok(`Açar yadda saxlanıldı — profil «${name}» (${maskKey(key)})`);
|
|
30
|
+
info(dim(` ${GLOBAL_FILE()}`));
|
|
31
|
+
info('');
|
|
32
|
+
info(who.trim());
|
|
33
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { writeFile } from 'node:fs/promises';
|
|
3
|
+
import { existsSync } from 'node:fs';
|
|
4
|
+
import { Mcp } from '../mcp.js';
|
|
5
|
+
import { resolveContext, readGlobal, writeGlobal, LOCAL_NAME, normalizeServer } from '../config.js';
|
|
6
|
+
import { collect, humanSize } from '../collect.js';
|
|
7
|
+
import { ok, info, dim, bold, cyan, maskKey } from '../ui.js';
|
|
8
|
+
|
|
9
|
+
/** `zedev status` — açar hara baxır, nə dərc olunub, qovluqda nə var. */
|
|
10
|
+
export async function status(args, flags) {
|
|
11
|
+
const dir = path.resolve(args[0] || '.');
|
|
12
|
+
const ctx = await resolveContext({ dir, key: flags.key, server: flags.server, profile: flags.profile });
|
|
13
|
+
|
|
14
|
+
info(`${bold('Server')} ${cyan(ctx.server)}`);
|
|
15
|
+
info(`${bold('Profil')} ${ctx.profile} ${dim(maskKey(ctx.key))}`);
|
|
16
|
+
info(`${bold('Qovluq')} ${dir}${ctx.localFile ? dim(` (${path.basename(ctx.localFile)})`) : ''}`);
|
|
17
|
+
info('');
|
|
18
|
+
|
|
19
|
+
const info_ = await new Mcp(ctx.server, ctx.key).call('project_info', {});
|
|
20
|
+
info(info_.trim());
|
|
21
|
+
|
|
22
|
+
const { accepted, skipped, total } = await collect(dir);
|
|
23
|
+
info('');
|
|
24
|
+
info(`${bold('Yerli qovluq')} ${accepted.length} fayl · ${humanSize(total)}${skipped.length ? dim(` · ${skipped.length} atılacaq`) : ''}`);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** `zedev init` — qovluğa hədəf faylı yazır. Açar BURAYA yazılmır (repo-ya düşməsin). */
|
|
28
|
+
export async function init(args, flags) {
|
|
29
|
+
const dir = path.resolve(args[0] || '.');
|
|
30
|
+
const file = path.join(dir, LOCAL_NAME);
|
|
31
|
+
if (existsSync(file) && !flags.force) throw new Error(`${LOCAL_NAME} artıq var. Əvəz etmək üçün --force.`);
|
|
32
|
+
|
|
33
|
+
const global = await readGlobal();
|
|
34
|
+
const profile = flags.profile || global.current;
|
|
35
|
+
if (!profile) throw new Error('Əvvəlcə `zedev login --key zedev_…` işlət.');
|
|
36
|
+
|
|
37
|
+
const data = { profile, publish: !!flags.publish };
|
|
38
|
+
await writeFile(file, `${JSON.stringify(data, null, 2)}\n`, 'utf8');
|
|
39
|
+
ok(`${LOCAL_NAME} yazıldı — profil «${profile}»${data.publish ? ', push dərc edir' : ''}`);
|
|
40
|
+
info(dim('Bu fayl sirr daşımır, komanda ilə paylaşıla bilər.'));
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** `zedev profiles` — hansı serverlərə açar var. */
|
|
44
|
+
export async function profiles() {
|
|
45
|
+
const cfg = await readGlobal();
|
|
46
|
+
const names = Object.keys(cfg.profiles);
|
|
47
|
+
if (names.length === 0) { info(dim('Profil yoxdur. `zedev login --key zedev_…`')); return; }
|
|
48
|
+
for (const n of names) {
|
|
49
|
+
const p = cfg.profiles[n];
|
|
50
|
+
info(`${n === cfg.current ? bold(`* ${n}`) : ` ${n}`} ${dim(p.server)} ${dim(maskKey(p.key))}`);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** `zedev logout [profil]` — açarı diskdən silir. */
|
|
55
|
+
export async function logout(args) {
|
|
56
|
+
const cfg = await readGlobal();
|
|
57
|
+
const name = args[0] || cfg.current;
|
|
58
|
+
if (!name || !cfg.profiles[name]) throw new Error(`Profil tapılmadı: ${name || '(yoxdur)'}`);
|
|
59
|
+
delete cfg.profiles[name];
|
|
60
|
+
if (cfg.current === name) cfg.current = Object.keys(cfg.profiles)[0] ?? null;
|
|
61
|
+
await writeGlobal(cfg);
|
|
62
|
+
ok(`«${name}» silindi.`);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** `zedev use <profil>` — cari profili dəyişir. */
|
|
66
|
+
export async function use(args) {
|
|
67
|
+
const name = args[0];
|
|
68
|
+
const cfg = await readGlobal();
|
|
69
|
+
if (!name || !cfg.profiles[name]) throw new Error(`Profil tapılmadı: ${name || '(ad verilmədi)'}`);
|
|
70
|
+
cfg.current = name;
|
|
71
|
+
await writeGlobal(cfg);
|
|
72
|
+
ok(`Cari profil: ${name} ${dim(normalizeServer(cfg.profiles[name].server))}`);
|
|
73
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { Mcp } from '../mcp.js';
|
|
3
|
+
import { resolveContext } from '../config.js';
|
|
4
|
+
import { collect, toPayload, humanSize, MAX_TOTAL_BYTES } from '../collect.js';
|
|
5
|
+
import { ok, info, warn, dim, bold, cyan, yellow } from '../ui.js';
|
|
6
|
+
|
|
7
|
+
const REASON = { ignored: 'nəzərə alınmır', tooBig: 'çox böyük', invalid: 'yolu yaramır' };
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* `zedev push [qovluq] [--publish] [--note "…"] [--delete-missing] [--dry-run]`
|
|
11
|
+
*
|
|
12
|
+
* Vebdəki yükləmə dialoqunun **eyni davranışı**: susqun halda heç nə silinmir, dərc
|
|
13
|
+
* ayrıca qərardır. Fərq yalnız girişdədir — brauzer əvəzinə qovluq.
|
|
14
|
+
*/
|
|
15
|
+
export async function push(args, flags) {
|
|
16
|
+
const dir = path.resolve(args[0] || '.');
|
|
17
|
+
const ctx = await resolveContext({ dir, key: flags.key, server: flags.server, profile: flags.profile });
|
|
18
|
+
|
|
19
|
+
const { accepted, skipped, total } = await collect(dir);
|
|
20
|
+
if (accepted.length === 0) throw new Error(`${dir} içində göndəriləsi fayl tapılmadı.`);
|
|
21
|
+
if (total > MAX_TOTAL_BYTES) {
|
|
22
|
+
throw new Error(`Ümumi həcm ${humanSize(total)} — limit ${humanSize(MAX_TOTAL_BYTES)}. Ağır qovluqları çıxar.`);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const deleteMissing = !!flags['delete-missing'];
|
|
26
|
+
const publish = flags.publish ?? ctx.local.publish ?? false;
|
|
27
|
+
|
|
28
|
+
info(`${bold(dir)} ${dim('→')} ${cyan(ctx.server)} ${dim(`(profil: ${ctx.profile})`)}`);
|
|
29
|
+
info(`${accepted.length} fayl · ${humanSize(total)}${skipped.length ? dim(` · ${skipped.length} atıldı`) : ''}`);
|
|
30
|
+
if (flags.verbose) {
|
|
31
|
+
for (const f of accepted) info(dim(` ${f.path} (${humanSize(f.size)})`));
|
|
32
|
+
for (const s of skipped) info(dim(` — ${s.path} (${REASON[s.reason] || s.reason})`));
|
|
33
|
+
}
|
|
34
|
+
// Silmə susqun deyil və həmişə səslə xəbərdarlıq edir — geri qaytarılmır.
|
|
35
|
+
if (deleteMissing) warn(yellow('--delete-missing: qovluqda olmayan fayllar qaralamadan SİLİNƏCƏK.'));
|
|
36
|
+
|
|
37
|
+
if (flags['dry-run']) {
|
|
38
|
+
info(dim('--dry-run: heç nə göndərilmədi.'));
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const mcp = new Mcp(ctx.server, ctx.key, { timeoutMs: 120_000 });
|
|
43
|
+
const payload = await toPayload(accepted);
|
|
44
|
+
const res = await mcp.call('standards_import', { files: payload, deleteMissing });
|
|
45
|
+
ok(res.trim());
|
|
46
|
+
|
|
47
|
+
if (!publish) {
|
|
48
|
+
info(dim('Qaralamaya yazıldı. Komandaya çatması üçün: `zedev push --publish` və ya platformada «Dərc et».'));
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const note = flags.note || `CLI · ${new Date().toISOString().slice(0, 16).replace('T', ' ')}`;
|
|
53
|
+
const pub = await mcp.call('standards_publish', { note });
|
|
54
|
+
ok(pub.trim());
|
|
55
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { watch as fsWatch } from 'node:fs';
|
|
3
|
+
import { Mcp } from '../mcp.js';
|
|
4
|
+
import { resolveContext } from '../config.js';
|
|
5
|
+
import { collect, toPayload, humanSize, IGNORED_DIRS, IGNORED_FILES } from '../collect.js';
|
|
6
|
+
import { ok, info, fail, dim, bold, cyan } from '../ui.js';
|
|
7
|
+
|
|
8
|
+
const DEBOUNCE_MS = 800;
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* `zedev watch [qovluq] [--publish]`
|
|
12
|
+
*
|
|
13
|
+
* Redaktə → qaralama. **Dərc susqun halda yoxdur**: yazı prosesində hər saxlama komandaya
|
|
14
|
+
* yeni versiya kimi getsəydi, versiya jurnalı mənasını itirərdi (bu, `--publish` ilə
|
|
15
|
+
* şüurlu seçimdir).
|
|
16
|
+
*
|
|
17
|
+
* ⚠️ Tam ağac hər dəfə göndərilir — `standards_import` idempotentdir və fərqi server
|
|
18
|
+
* hesablayır. Fərqi klientdə hesablamaq (hash keşi) sürətli olardı, amma iki ayrı
|
|
19
|
+
* «həqiqət» yaradardı; qovluq ölçüsü bizdə kiçikdir (onlarla fayl).
|
|
20
|
+
*/
|
|
21
|
+
export async function watch(args, flags) {
|
|
22
|
+
const dir = path.resolve(args[0] || '.');
|
|
23
|
+
const ctx = await resolveContext({ dir, key: flags.key, server: flags.server, profile: flags.profile });
|
|
24
|
+
const mcp = new Mcp(ctx.server, ctx.key, { timeoutMs: 120_000 });
|
|
25
|
+
|
|
26
|
+
info(`${bold(dir)} ${dim('→')} ${cyan(ctx.server)} ${dim(`(profil: ${ctx.profile})`)}`);
|
|
27
|
+
info(dim('İzlənir. Dayandırmaq: Ctrl+C'));
|
|
28
|
+
|
|
29
|
+
let timer = null;
|
|
30
|
+
let running = false;
|
|
31
|
+
let again = false;
|
|
32
|
+
|
|
33
|
+
const send = async () => {
|
|
34
|
+
if (running) { again = true; return; }
|
|
35
|
+
running = true;
|
|
36
|
+
try {
|
|
37
|
+
const { accepted, total } = await collect(dir);
|
|
38
|
+
if (accepted.length === 0) { info(dim('fayl yoxdur')); return; }
|
|
39
|
+
const res = await mcp.call('standards_import', { files: await toPayload(accepted), deleteMissing: false });
|
|
40
|
+
ok(`${new Date().toLocaleTimeString()} · ${res.trim().split('\n')[0]} ${dim(`(${accepted.length} fayl · ${humanSize(total)})`)}`);
|
|
41
|
+
if (flags.publish) {
|
|
42
|
+
const pub = await mcp.call('standards_publish', { note: flags.note || 'CLI watch' });
|
|
43
|
+
ok(pub.trim().split('\n')[0]);
|
|
44
|
+
}
|
|
45
|
+
} catch (e) {
|
|
46
|
+
// İzləmə davam edir: şəbəkə qırılması və ya müvəqqəti server xətası seansı öldürməsin.
|
|
47
|
+
fail(e.message);
|
|
48
|
+
} finally {
|
|
49
|
+
running = false;
|
|
50
|
+
if (again) { again = false; send(); }
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
const interesting = (file) => {
|
|
55
|
+
if (!file) return true;
|
|
56
|
+
const segs = String(file).replace(/\\/g, '/').split('/');
|
|
57
|
+
if (segs.some((s) => IGNORED_DIRS.has(s))) return false;
|
|
58
|
+
return !IGNORED_FILES.has(segs[segs.length - 1]);
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
let watcher;
|
|
62
|
+
try {
|
|
63
|
+
watcher = fsWatch(dir, { recursive: true }, (_event, file) => {
|
|
64
|
+
if (!interesting(file)) return;
|
|
65
|
+
clearTimeout(timer);
|
|
66
|
+
timer = setTimeout(send, DEBOUNCE_MS);
|
|
67
|
+
});
|
|
68
|
+
} catch (e) {
|
|
69
|
+
// Köhnə Linux nüvələrində `recursive` dəstəklənmir — səbəbi açıq deyirik.
|
|
70
|
+
throw new Error(`Qovluq izlənə bilmədi: ${e.message}. Bu platformada \`zedev push\` işlət.`);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
await send(); // ilk vəziyyət dərhal göndərilir — "işləyir?" sualı qalmasın
|
|
74
|
+
|
|
75
|
+
process.on('SIGINT', () => { watcher.close(); info(dim('\nDayandırıldı.')); process.exit(0); });
|
|
76
|
+
await new Promise(() => {}); // Ctrl+C-yə qədər
|
|
77
|
+
}
|
package/src/config.js
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { readFile, writeFile, mkdir, chmod } from 'node:fs/promises';
|
|
2
|
+
import { existsSync } from 'node:fs';
|
|
3
|
+
import { homedir } from 'node:os';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Konfiqurasiya İKİ yerə bölünür və bu bölgü təhlükəsizlik qərarıdır:
|
|
8
|
+
*
|
|
9
|
+
* · **Açar** — `~/.zedev/config.json` (istifadəçinin ev qovluğu, 0600).
|
|
10
|
+
* Repo qovluğuna heç vaxt yazılmır: orada olsaydı, ilk `git add .` onu commit edərdi.
|
|
11
|
+
* · **Hədəf** — qovluqdakı `.zedev.json` (server + profil adı).
|
|
12
|
+
* Bu, komanda ilə paylaşıla bilər, çünki sirr daşımır.
|
|
13
|
+
*
|
|
14
|
+
* Üstünlük sırası: bayraq → mühit dəyişəni → `.zedev.json` → qlobal konfiq.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
export const GLOBAL_DIR = () => process.env.ZEDEV_HOME || path.join(homedir(), '.zedev');
|
|
18
|
+
export const GLOBAL_FILE = () => path.join(GLOBAL_DIR(), 'config.json');
|
|
19
|
+
export const LOCAL_NAME = '.zedev.json';
|
|
20
|
+
|
|
21
|
+
const readJson = async (file) => {
|
|
22
|
+
try { return JSON.parse(await readFile(file, 'utf8')); }
|
|
23
|
+
catch (e) {
|
|
24
|
+
if (e.code === 'ENOENT') return null;
|
|
25
|
+
throw new Error(`${file} oxunmadı: ${e.message}`);
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export async function readGlobal() {
|
|
30
|
+
const raw = await readJson(GLOBAL_FILE());
|
|
31
|
+
if (!raw) return { profiles: {}, current: null };
|
|
32
|
+
return { profiles: raw.profiles ?? {}, current: raw.current ?? null };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export async function writeGlobal(cfg) {
|
|
36
|
+
await mkdir(GLOBAL_DIR(), { recursive: true });
|
|
37
|
+
await writeFile(GLOBAL_FILE(), `${JSON.stringify(cfg, null, 2)}\n`, 'utf8');
|
|
38
|
+
// Windows-da POSIX rejimi yoxdur — səssiz keçirik, faylın özü istifadəçi profilindədir.
|
|
39
|
+
try { await chmod(GLOBAL_FILE(), 0o600); } catch { /* Windows */ }
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** `.zedev.json`-u qovluqdan yuxarı doğru axtarır — alt qovluqdan da `zedev push` işləsin. */
|
|
43
|
+
export async function findLocal(startDir) {
|
|
44
|
+
let dir = path.resolve(startDir);
|
|
45
|
+
for (;;) {
|
|
46
|
+
const file = path.join(dir, LOCAL_NAME);
|
|
47
|
+
if (existsSync(file)) return { file, data: (await readJson(file)) ?? {} };
|
|
48
|
+
const up = path.dirname(dir);
|
|
49
|
+
if (up === dir) return null;
|
|
50
|
+
dir = up;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Serverin sonundakı `/` və təsadüfi `/api/mcp` quyruğu atılır — ikisi də tez-tez yapışdırılır. */
|
|
55
|
+
export function normalizeServer(raw) {
|
|
56
|
+
if (!raw) return null;
|
|
57
|
+
let s = String(raw).trim().replace(/\/+$/, '');
|
|
58
|
+
if (!/^https?:\/\//i.test(s)) s = `https://${s}`;
|
|
59
|
+
return s.replace(/\/api\/mcp$/i, '');
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Əmrin işləyəcəyi kontekst. Açar tapılmasa danışan xəta atılır — CLI-ın ən çox
|
|
64
|
+
* rast gəlinən ilk təcrübəsi məhz budur.
|
|
65
|
+
*/
|
|
66
|
+
export async function resolveContext({ dir = process.cwd(), key, server, profile } = {}) {
|
|
67
|
+
const local = await findLocal(dir);
|
|
68
|
+
const global = await readGlobal();
|
|
69
|
+
|
|
70
|
+
const name = profile || process.env.ZEDEV_PROFILE || local?.data?.profile || global.current;
|
|
71
|
+
const saved = (name && global.profiles[name]) || null;
|
|
72
|
+
|
|
73
|
+
const finalServer = normalizeServer(
|
|
74
|
+
server || process.env.ZEDEV_SERVER || local?.data?.server || saved?.server,
|
|
75
|
+
);
|
|
76
|
+
const finalKey = key || process.env.ZEDEV_KEY || saved?.key;
|
|
77
|
+
|
|
78
|
+
if (!finalKey) {
|
|
79
|
+
throw new Error(
|
|
80
|
+
'Açar yoxdur. Platformada dəstin «CLI açarı» düyməsindən açar al, sonra:\n'
|
|
81
|
+
+ ' zedev login --key zedev_…',
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
if (!finalServer) throw new Error('Server bilinmir. `zedev login --key … --server https://…` yaz.');
|
|
85
|
+
|
|
86
|
+
return {
|
|
87
|
+
server: finalServer,
|
|
88
|
+
key: finalKey,
|
|
89
|
+
profile: name || 'default',
|
|
90
|
+
localFile: local?.file ?? null,
|
|
91
|
+
local: local?.data ?? {},
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** Profil adı serverin host hissəsindən götürülür: `dev.zestudio.az` → `dev`. */
|
|
96
|
+
export function profileNameFor(server) {
|
|
97
|
+
try { return new URL(server).hostname.split('.')[0] || 'default'; }
|
|
98
|
+
catch { return 'default'; }
|
|
99
|
+
}
|
package/src/mcp.js
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ZEDEV MCP klienti — asılılıqsız, `fetch` üzərində.
|
|
3
|
+
*
|
|
4
|
+
* Server **stateless HTTP** transportu işlədir: hər sorğu müstəqildir, ona görə
|
|
5
|
+
* alət çağırışından əvvəl hər dəfə `initialize` gedir (sessiya saxlanmır).
|
|
6
|
+
* Cavab ya JSON, ya da SSE (`data: {...}`) olur — hər ikisi oxunur.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const RPC = '2.0';
|
|
10
|
+
|
|
11
|
+
export class McpError extends Error {}
|
|
12
|
+
|
|
13
|
+
export class Mcp {
|
|
14
|
+
constructor(server, key, { timeoutMs = 60_000 } = {}) {
|
|
15
|
+
this.url = `${server}/api/mcp`;
|
|
16
|
+
this.key = key;
|
|
17
|
+
this.timeoutMs = timeoutMs;
|
|
18
|
+
this.id = 0;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
async #send(method, params) {
|
|
22
|
+
const body = JSON.stringify({ jsonrpc: RPC, id: ++this.id, method, params: params ?? {} });
|
|
23
|
+
const ac = new AbortController();
|
|
24
|
+
const timer = setTimeout(() => ac.abort(), this.timeoutMs);
|
|
25
|
+
let res;
|
|
26
|
+
try {
|
|
27
|
+
res = await fetch(this.url, {
|
|
28
|
+
method: 'POST',
|
|
29
|
+
headers: {
|
|
30
|
+
'Content-Type': 'application/json',
|
|
31
|
+
// Server SSE qaytara bilər — hər ikisini qəbul etdiyimizi bildiririk.
|
|
32
|
+
Accept: 'application/json, text/event-stream',
|
|
33
|
+
Authorization: `Bearer ${this.key}`,
|
|
34
|
+
},
|
|
35
|
+
body,
|
|
36
|
+
signal: ac.signal,
|
|
37
|
+
});
|
|
38
|
+
} catch (e) {
|
|
39
|
+
if (e.name === 'AbortError') throw new McpError(`Server ${this.timeoutMs / 1000}s cavab vermədi: ${this.url}`);
|
|
40
|
+
throw new McpError(`Serverə qoşulmaq olmadı (${this.url}): ${e.message}`);
|
|
41
|
+
} finally {
|
|
42
|
+
clearTimeout(timer);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const text = await res.text();
|
|
46
|
+
if (res.status === 401 || res.status === 403) {
|
|
47
|
+
throw new McpError('Açar qəbul olunmadı (401/403). Ləğv edilib və ya başqa serverə aiddir.');
|
|
48
|
+
}
|
|
49
|
+
if (!res.ok && !text) throw new McpError(`Server ${res.status} qaytardı.`);
|
|
50
|
+
|
|
51
|
+
const line = text.split('\n').find((l) => l.startsWith('data:'));
|
|
52
|
+
let json;
|
|
53
|
+
try { json = JSON.parse(line ? line.slice(5).trim() : text); }
|
|
54
|
+
catch { throw new McpError(`Cavab oxunmadı (${res.status}): ${text.slice(0, 200)}`); }
|
|
55
|
+
|
|
56
|
+
if (json.error) throw new McpError(json.error.message || JSON.stringify(json.error));
|
|
57
|
+
return json.result;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async #init() {
|
|
61
|
+
await this.#send('initialize', {
|
|
62
|
+
protocolVersion: '2024-11-05',
|
|
63
|
+
capabilities: {},
|
|
64
|
+
clientInfo: { name: 'zedev-cli', version: '0.1.0' },
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** Alət çağırışı. Alət səviyyəsindəki xəta `isError` ilə gəlir — onu da atırıq. */
|
|
69
|
+
async call(name, args = {}) {
|
|
70
|
+
await this.#init();
|
|
71
|
+
const result = await this.#send('tools/call', { name, arguments: args });
|
|
72
|
+
const text = (result?.content ?? []).map((c) => c.text).filter(Boolean).join('\n');
|
|
73
|
+
if (result?.isError) throw new McpError(text || `\`${name}\` xəta qaytardı.`);
|
|
74
|
+
return text;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
async tools() {
|
|
78
|
+
await this.#init();
|
|
79
|
+
const result = await this.#send('tools/list', {});
|
|
80
|
+
return (result?.tools ?? []).map((x) => x.name);
|
|
81
|
+
}
|
|
82
|
+
}
|
package/src/ui.js
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/** Terminal çıxışı — asılılıqsız. Rəng yalnız əsl terminalda (boru/CI-da düz mətn). */
|
|
2
|
+
|
|
3
|
+
const useColor = process.stdout.isTTY && !process.env.NO_COLOR;
|
|
4
|
+
const wrap = (code) => (s) => (useColor ? `[${code}m${s}[0m` : String(s));
|
|
5
|
+
|
|
6
|
+
export const bold = wrap('1');
|
|
7
|
+
export const dim = wrap('2');
|
|
8
|
+
export const green = wrap('32');
|
|
9
|
+
export const yellow = wrap('33');
|
|
10
|
+
export const red = wrap('31');
|
|
11
|
+
export const cyan = wrap('36');
|
|
12
|
+
|
|
13
|
+
export const info = (msg) => console.log(msg);
|
|
14
|
+
export const ok = (msg) => console.log(`${green('✓')} ${msg}`);
|
|
15
|
+
export const warn = (msg) => console.log(`${yellow('!')} ${msg}`);
|
|
16
|
+
export const fail = (msg) => console.error(`${red('✗')} ${msg}`);
|
|
17
|
+
|
|
18
|
+
/** Açar heç vaxt tam çap olunmur — terminal jurnalı və ekran görüntüsü sızma yoludur. */
|
|
19
|
+
export const maskKey = (key) => (key && key.length > 12 ? `${key.slice(0, 10)}…${key.slice(-4)}` : '…');
|