@konce-pt/ui 0.2.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 konce.pt
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 ADDED
@@ -0,0 +1,32 @@
1
+ # @konce-pt/ui
2
+
3
+ CLI Koncept UI — instaluje **bloki** (gotowe fragmenty UI) do projektu Angular w modelu
4
+ copy-paste. Po instalacji blok jest Twoim kodem.
5
+
6
+ ```bash
7
+ npx @konce-pt/ui init
8
+ npx @konce-pt/ui add nip-lookup-field
9
+ ```
10
+
11
+ ## Komendy
12
+
13
+ | Komenda | Opis |
14
+ |---|---|
15
+ | `init` | Tworzy `konce.json` (`--registry --blocks-dir --alias --prefix --theme --force`) |
16
+ | `add <nazwa...>` | Instaluje blok(i) + zależności registry (`--overwrite --dry-run --path --registry`) |
17
+ | `list [--category=…]` | Lista dostępnych bloków |
18
+
19
+ ## Jak działa `add`
20
+
21
+ 1. Rozwiązuje zależności registry rekurencyjnie (wykrywa cykle).
22
+ 2. Sprawdza zależności npm — brak: ostrzega, za niska wersja: przerywa.
23
+ 3. Przepisuje `@/` → `alias`, selektory `kpt-` → `prefix` (tokeny `--kpt-*` zostają nietknięte).
24
+ 4. Konflikt pliku: identyczny pomija, różny zapisuje jako `.new` (lub `--overwrite`).
25
+ 5. Zapisuje dopiero po pobraniu wszystkich plików; odnotowuje `installedBlocks`.
26
+ 6. Wypisuje: dodane pliki, przykład użycia, porty do podpięcia, uwagi.
27
+
28
+ `--dry-run` przechodzi całą ścieżkę bez zapisu — idealne dla agentów AI do podglądu skutków.
29
+
30
+ ## Licencja
31
+
32
+ MIT © konce.pt
@@ -0,0 +1,193 @@
1
+ import path from 'node:path';
2
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
3
+ import { requireConfig, writeConfig } from "../config.js";
4
+ import { loadIndex, loadBlockMeta, loadBlockFile } from "../registry.js";
5
+ import { flag, hasFlag, log, UserError } from "../util.js";
6
+ export async function addCommand(args) {
7
+ const requested = args.positionals;
8
+ if (requested.length === 0)
9
+ throw new UserError('Podaj nazwę bloku: add <nazwa...>');
10
+ const { path: configPath, config } = requireConfig(process.cwd());
11
+ const projectRoot = path.dirname(configPath);
12
+ const base = flag(args, 'registry') ?? config.registry;
13
+ const overwrite = hasFlag(args, 'overwrite');
14
+ const dryRun = hasFlag(args, 'dry-run');
15
+ const targetDirRel = flag(args, 'path') ?? config.blocksDir;
16
+ const index = await loadIndex(base);
17
+ // 1) Rozwiąż zależności registry rekurencyjnie (kolejność zależnościowa, wykryj cykle).
18
+ const order = [];
19
+ const done = new Set();
20
+ const stack = new Set();
21
+ const visit = (name) => {
22
+ if (done.has(name))
23
+ return;
24
+ if (stack.has(name))
25
+ throw new UserError(`Cykl zależności registry wokół bloku "${name}".`);
26
+ const entry = index.blocks.find((b) => b.name === name);
27
+ if (!entry)
28
+ throw new UserError(unknownBlockMessage(name, index));
29
+ stack.add(name);
30
+ for (const dep of entry.registryDependencies)
31
+ visit(dep);
32
+ stack.delete(name);
33
+ done.add(name);
34
+ order.push(name);
35
+ };
36
+ for (const name of requested)
37
+ visit(name);
38
+ // 2) Pomiń już zainstalowane (chyba że --overwrite).
39
+ const toInstall = order.filter((name) => overwrite || !config.installedBlocks[name]);
40
+ const skippedInstalled = order.filter((name) => !overwrite && config.installedBlocks[name]);
41
+ for (const name of skippedInstalled)
42
+ log.step(`${name} — już zainstalowany (pomijam; --overwrite by nadpisać)`);
43
+ if (toInstall.length === 0) {
44
+ log.info('Nic do zrobienia.');
45
+ return;
46
+ }
47
+ // 3) Wczytaj definicje, sprawdź zależności npm.
48
+ const metas = new Map();
49
+ for (const name of toInstall)
50
+ metas.set(name, await loadBlockMeta(base, name));
51
+ checkNpmDependencies(metas, projectRoot);
52
+ // 4) Pobierz WSZYSTKIE pliki i zaplanuj zapis (instalacja atomowa — nic nie zapisujemy wcześniej).
53
+ const plan = [];
54
+ for (const name of toInstall) {
55
+ const meta = metas.get(name);
56
+ for (const file of meta.files) {
57
+ const raw = await loadBlockFile(base, name, file.path);
58
+ const content = rewriteImports(raw, config);
59
+ const absolute = path.resolve(projectRoot, targetDirRel, name, file.path);
60
+ plan.push(planWrite(name, absolute, content, projectRoot, overwrite));
61
+ }
62
+ }
63
+ // 5) Zapis (albo raport dry-run).
64
+ if (dryRun) {
65
+ log.info('— dry-run: żadnych zapisów —');
66
+ for (const p of plan)
67
+ log.step(`${labelFor(p.action)} ${p.target}`);
68
+ }
69
+ else {
70
+ for (const p of plan) {
71
+ if (p.action === 'skip-identical') {
72
+ log.step(`bez zmian ${p.target}`);
73
+ continue;
74
+ }
75
+ const dest = p.action === 'conflict-new' ? `${p.absolute}.new` : p.absolute;
76
+ mkdirSync(path.dirname(dest), { recursive: true });
77
+ writeFileSync(dest, p.content);
78
+ log.step(`${labelFor(p.action)} ${path.relative(projectRoot, dest)}`);
79
+ }
80
+ const now = new Date().toISOString().slice(0, 10);
81
+ for (const name of toInstall) {
82
+ config.installedBlocks[name] = { version: index.libraryVersion, installedAt: now };
83
+ }
84
+ writeConfig(configPath, config);
85
+ }
86
+ printSummary(toInstall, metas, dryRun);
87
+ }
88
+ // — pomocnicze —
89
+ function planWrite(block, absolute, content, projectRoot, overwrite) {
90
+ const target = path.relative(projectRoot, absolute);
91
+ if (!existsSync(absolute))
92
+ return { block, target, absolute, content, action: 'write' };
93
+ const current = readFileSync(absolute, 'utf8');
94
+ if (current === content)
95
+ return { block, target, absolute, content, action: 'skip-identical' };
96
+ if (overwrite)
97
+ return { block, target, absolute, content, action: 'overwrite' };
98
+ return { block, target, absolute, content, action: 'conflict-new' };
99
+ }
100
+ function labelFor(action) {
101
+ switch (action) {
102
+ case 'write': return 'nowy ';
103
+ case 'overwrite': return 'nadpisano';
104
+ case 'conflict-new': return 'konflikt→.new';
105
+ case 'skip-identical': return 'bez zmian';
106
+ }
107
+ }
108
+ /** Przepisuje placeholdery: `@/` → alias, selektory `kpt-` → prefix (nie rusza `--kpt-`). */
109
+ function rewriteImports(content, config) {
110
+ let out = content.replaceAll('@/', `${config.alias}/`);
111
+ if (config.prefix !== 'kpt') {
112
+ out = out.replace(/(?<![-\w])kpt-/g, `${config.prefix}-`);
113
+ }
114
+ return out;
115
+ }
116
+ function checkNpmDependencies(metas, projectRoot) {
117
+ const pkgPath = path.join(projectRoot, 'package.json');
118
+ const installed = {};
119
+ if (existsSync(pkgPath)) {
120
+ const pkg = JSON.parse(readFileSync(pkgPath, 'utf8'));
121
+ Object.assign(installed, pkg.dependencies ?? {}, pkg.devDependencies ?? {});
122
+ }
123
+ const missing = new Set();
124
+ for (const meta of metas.values()) {
125
+ for (const [name, range] of Object.entries(meta.dependencies?.npm ?? {})) {
126
+ const have = installed[name];
127
+ if (!have) {
128
+ missing.add(`${name}@${range}`);
129
+ continue;
130
+ }
131
+ // Specyfikatory bez konkretnej wersji (workspace:*, *, latest, git/file:) — nie oceniamy.
132
+ if (!/\d/.test(have))
133
+ continue;
134
+ if (belowMinimum(have, range)) {
135
+ throw new UserError(`Zależność ${name} jest w wersji ${have}, blok wymaga ${range}. ` +
136
+ `Zaktualizuj ją samodzielnie i uruchom ponownie.`);
137
+ }
138
+ }
139
+ }
140
+ if (missing.size > 0) {
141
+ log.warn(`Brakujące zależności npm — zainstaluj je: ${[...missing].join(' ')}`);
142
+ }
143
+ }
144
+ function toTuple(version) {
145
+ const [maj = 0, min = 0, patch = 0] = version.replace(/^[^\d]*/, '').split('.').map((n) => parseInt(n, 10) || 0);
146
+ return [maj, min, patch];
147
+ }
148
+ /** Prosty test: czy `have` jest poniżej minimum z zakresu (`^`, `~`, `>=`). */
149
+ function belowMinimum(have, range) {
150
+ const [hM, hm, hp] = toTuple(have);
151
+ const [rM, rm, rp] = toTuple(range);
152
+ if (hM !== rM)
153
+ return hM < rM;
154
+ if (hm !== rm)
155
+ return hm < rm;
156
+ return hp < rp;
157
+ }
158
+ function printSummary(installed, metas, dryRun) {
159
+ log.info(`\n${dryRun ? 'Zainstalowałbym' : 'Zainstalowano'}: ${installed.join(', ')}`);
160
+ for (const name of installed) {
161
+ const meta = metas.get(name);
162
+ if (meta.usage) {
163
+ log.info(`\n${name}:`);
164
+ log.step(meta.usage.replace(/\n/g, '\n '));
165
+ }
166
+ const ports = meta.api?.ports ?? [];
167
+ for (const port of ports) {
168
+ log.warn(`port do podpięcia: ${port.token} — ${port.signature}${port.stubProvided ? ' (stub w komplecie)' : ''}`);
169
+ }
170
+ if (meta.notes)
171
+ log.step(`uwaga: ${meta.notes}`);
172
+ }
173
+ }
174
+ function unknownBlockMessage(name, index) {
175
+ const nearest = index.blocks
176
+ .map((b) => ({ name: b.name, d: levenshtein(name, b.name) }))
177
+ .sort((a, b) => a.d - b.d)[0];
178
+ const hint = nearest && nearest.d <= 3 ? ` Czy chodziło o "${nearest.name}"?` : '';
179
+ return `Nie znam bloku "${name}".${hint} Zobacz: npx @konce-pt/ui list`;
180
+ }
181
+ function levenshtein(a, b) {
182
+ const dp = Array.from({ length: a.length + 1 }, (_, i) => i);
183
+ for (let j = 1; j <= b.length; j++) {
184
+ let prev = dp[0];
185
+ dp[0] = j;
186
+ for (let i = 1; i <= a.length; i++) {
187
+ const tmp = dp[i];
188
+ dp[i] = Math.min(dp[i] + 1, dp[i - 1] + 1, prev + (a[i - 1] === b[j - 1] ? 0 : 1));
189
+ prev = tmp;
190
+ }
191
+ }
192
+ return dp[a.length];
193
+ }
@@ -0,0 +1,27 @@
1
+ import path from 'node:path';
2
+ import { existsSync } from 'node:fs';
3
+ import { CONFIG_FILE, DEFAULT_CONFIG, writeConfig } from "../config.js";
4
+ import { flag, hasFlag, log, UserError } from "../util.js";
5
+ export function initCommand(args) {
6
+ const cwd = process.cwd();
7
+ const configPath = path.join(cwd, CONFIG_FILE);
8
+ if (existsSync(configPath) && !hasFlag(args, 'force')) {
9
+ throw new UserError(`${CONFIG_FILE} już istnieje. Użyj --force, aby nadpisać.`);
10
+ }
11
+ const config = {
12
+ ...DEFAULT_CONFIG,
13
+ registry: flag(args, 'registry') ?? DEFAULT_CONFIG.registry,
14
+ blocksDir: flag(args, 'blocks-dir') ?? DEFAULT_CONFIG.blocksDir,
15
+ alias: flag(args, 'alias') ?? DEFAULT_CONFIG.alias,
16
+ prefix: flag(args, 'prefix') ?? DEFAULT_CONFIG.prefix,
17
+ theme: flag(args, 'theme') ?? DEFAULT_CONFIG.theme,
18
+ installedBlocks: {},
19
+ };
20
+ writeConfig(configPath, config);
21
+ log.ok(`Utworzono ${CONFIG_FILE}`);
22
+ log.step(`registry: ${config.registry}`);
23
+ log.step(`blocksDir: ${config.blocksDir}`);
24
+ log.step(`alias: ${config.alias}`);
25
+ log.step(`prefix: ${config.prefix}`);
26
+ log.info('\nDodaj blok: npx @konce-pt/ui add <nazwa>');
27
+ }
@@ -0,0 +1,30 @@
1
+ import { DEFAULT_CONFIG, findConfigPath, readConfig } from "../config.js";
2
+ import { loadIndex } from "../registry.js";
3
+ import { flag, log } from "../util.js";
4
+ /** Baza registry: flaga --registry > konce.json > domyślna. */
5
+ function resolveRegistry(args) {
6
+ const explicit = flag(args, 'registry');
7
+ if (explicit)
8
+ return explicit;
9
+ const configPath = findConfigPath(process.cwd());
10
+ if (configPath)
11
+ return readConfig(configPath).registry;
12
+ return DEFAULT_CONFIG.registry;
13
+ }
14
+ export async function listCommand(args) {
15
+ const base = resolveRegistry(args);
16
+ const category = flag(args, 'category');
17
+ const index = await loadIndex(base);
18
+ const blocks = category ? index.blocks.filter((b) => b.category === category) : index.blocks;
19
+ if (blocks.length === 0) {
20
+ log.warn(category ? `Brak bloków w kategorii "${category}".` : 'Registry nie zawiera bloków.');
21
+ return;
22
+ }
23
+ log.info(`Bloki @konce-pt (biblioteka ${index.libraryVersion}):\n`);
24
+ const width = Math.max(...blocks.map((b) => b.name.length));
25
+ for (const b of blocks) {
26
+ log.info(` ${b.name.padEnd(width)} ${b.title} [${b.category}]`);
27
+ log.info(` ${' '.repeat(width)} ${b.description}`);
28
+ }
29
+ log.info('\nZainstaluj: npx @konce-pt/ui add <nazwa>');
30
+ }
package/dist/config.js ADDED
@@ -0,0 +1,40 @@
1
+ import { existsSync, readFileSync, writeFileSync } from 'node:fs';
2
+ import path from 'node:path';
3
+ import { UserError } from "./util.js";
4
+ export const CONFIG_FILE = 'konce.json';
5
+ export const DEFAULT_CONFIG = {
6
+ $schema: 'https://ui.konce.pt/cli/konce-config-schema.json',
7
+ registry: 'https://ui.konce.pt/cli',
8
+ blocksDir: 'src/app/shared/blocks',
9
+ alias: '@shared/blocks',
10
+ prefix: 'kpt',
11
+ };
12
+ /** Szuka konce.json w górę drzewa od `start`. Zwraca ścieżkę lub null. */
13
+ export function findConfigPath(start) {
14
+ let dir = path.resolve(start);
15
+ for (;;) {
16
+ const candidate = path.join(dir, CONFIG_FILE);
17
+ if (existsSync(candidate))
18
+ return candidate;
19
+ const parent = path.dirname(dir);
20
+ if (parent === dir)
21
+ return null;
22
+ dir = parent;
23
+ }
24
+ }
25
+ export function readConfig(configPath) {
26
+ const cfg = JSON.parse(readFileSync(configPath, 'utf8'));
27
+ cfg.installedBlocks ??= {};
28
+ return cfg;
29
+ }
30
+ export function writeConfig(configPath, cfg) {
31
+ writeFileSync(configPath, JSON.stringify(cfg, null, 2) + '\n');
32
+ }
33
+ /** Wczytuje konfigurację albo przerywa z podpowiedzią o `init`. */
34
+ export function requireConfig(cwd) {
35
+ const configPath = findConfigPath(cwd);
36
+ if (!configPath) {
37
+ throw new UserError(`Brak ${CONFIG_FILE}. Uruchom najpierw: npx @konce-pt/ui init`);
38
+ }
39
+ return { path: configPath, config: readConfig(configPath) };
40
+ }
package/dist/index.js ADDED
@@ -0,0 +1,46 @@
1
+ #!/usr/bin/env node
2
+ import { parseArgs, log, UserError } from "./util.js";
3
+ import { initCommand } from "./commands/init.js";
4
+ import { listCommand } from "./commands/list.js";
5
+ import { addCommand } from "./commands/add.js";
6
+ const HELP = `Koncept UI — CLI bloków.
7
+
8
+ Użycie:
9
+ npx @konce-pt/ui <komenda> [opcje]
10
+
11
+ Komendy:
12
+ init utwórz konce.json (--registry --blocks-dir --alias --prefix --theme --force)
13
+ add <nazwa...> zainstaluj blok(i) + zależności (--overwrite --dry-run --path --registry)
14
+ list [--category=...] pokaż dostępne bloki (--registry)
15
+
16
+ Kody wyjścia: 0 ok, 1 błąd.`;
17
+ async function main() {
18
+ const [command, ...rest] = process.argv.slice(2);
19
+ const args = parseArgs(rest);
20
+ switch (command) {
21
+ case 'init':
22
+ initCommand(args);
23
+ break;
24
+ case 'add':
25
+ await addCommand(args);
26
+ break;
27
+ case 'list':
28
+ await listCommand(args);
29
+ break;
30
+ case undefined:
31
+ case '--help':
32
+ case '-h':
33
+ case 'help':
34
+ log.info(HELP);
35
+ break;
36
+ default:
37
+ throw new UserError(`Nieznana komenda "${command}". Zobacz: npx @konce-pt/ui --help`);
38
+ }
39
+ }
40
+ main().catch((err) => {
41
+ if (err instanceof UserError)
42
+ log.error(err.message);
43
+ else
44
+ log.error(err?.stack ?? String(err));
45
+ process.exitCode = 1;
46
+ });
@@ -0,0 +1,26 @@
1
+ import { existsSync, readFileSync } from 'node:fs';
2
+ import path from 'node:path';
3
+ import { UserError } from "./util.js";
4
+ const isHttp = (base) => /^https?:\/\//.test(base);
5
+ async function fetchText(base, relPath) {
6
+ if (isHttp(base)) {
7
+ const url = `${base.replace(/\/$/, '')}/${relPath}`;
8
+ const res = await fetch(url);
9
+ if (!res.ok)
10
+ throw new UserError(`Nie udało się pobrać ${url} (HTTP ${res.status}).`);
11
+ return res.text();
12
+ }
13
+ const file = path.resolve(base, relPath);
14
+ if (!existsSync(file))
15
+ throw new UserError(`Brak pliku w registry: ${file}`);
16
+ return readFileSync(file, 'utf8');
17
+ }
18
+ export async function loadIndex(base) {
19
+ return JSON.parse(await fetchText(base, 'index.json'));
20
+ }
21
+ export async function loadBlockMeta(base, name) {
22
+ return JSON.parse(await fetchText(base, `blocks/${name}.json`));
23
+ }
24
+ export async function loadBlockFile(base, name, filePath) {
25
+ return fetchText(base, `blocks/${name}/${filePath}`);
26
+ }
package/dist/util.js ADDED
@@ -0,0 +1,35 @@
1
+ // Drobne narzędzia CLI: parsowanie flag i logowanie. Zero zależności.
2
+ export function parseArgs(argv) {
3
+ const positionals = [];
4
+ const flags = new Map();
5
+ for (const arg of argv) {
6
+ if (arg.startsWith('--')) {
7
+ const eq = arg.indexOf('=');
8
+ if (eq === -1)
9
+ flags.set(arg.slice(2), true);
10
+ else
11
+ flags.set(arg.slice(2, eq), arg.slice(eq + 1));
12
+ }
13
+ else {
14
+ positionals.push(arg);
15
+ }
16
+ }
17
+ return { positionals, flags };
18
+ }
19
+ export function flag(args, name) {
20
+ const value = args.flags.get(name);
21
+ return typeof value === 'string' ? value : undefined;
22
+ }
23
+ export function hasFlag(args, name) {
24
+ return args.flags.has(name);
25
+ }
26
+ export const log = {
27
+ info: (msg) => console.log(msg),
28
+ step: (msg) => console.log(` ${msg}`),
29
+ ok: (msg) => console.log(`✓ ${msg}`),
30
+ warn: (msg) => console.warn(`! ${msg}`),
31
+ error: (msg) => console.error(`✗ ${msg}`),
32
+ };
33
+ /** Błąd, którego komunikat wystarczy pokazać userowi (bez stack trace). */
34
+ export class UserError extends Error {
35
+ }
package/package.json ADDED
@@ -0,0 +1,47 @@
1
+ {
2
+ "name": "@konce-pt/ui",
3
+ "version": "0.2.0",
4
+ "description": "CLI Koncept UI — instaluje bloki (copy-paste) do projektu Angular: init, add, list.",
5
+ "license": "MIT",
6
+ "author": "konce.pt",
7
+ "homepage": "https://gitlab.com/konce-pt/koncept-ui#readme",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://gitlab.com/konce-pt/koncept-ui.git",
11
+ "directory": "packages/cli"
12
+ },
13
+ "bugs": {
14
+ "url": "https://gitlab.com/konce-pt/koncept-ui/-/issues"
15
+ },
16
+ "keywords": [
17
+ "angular",
18
+ "cli",
19
+ "blocks",
20
+ "registry",
21
+ "koncept-ui"
22
+ ],
23
+ "type": "module",
24
+ "bin": {
25
+ "konce": "./dist/index.js"
26
+ },
27
+ "files": [
28
+ "dist"
29
+ ],
30
+ "devDependencies": {
31
+ "typescript": "~6.0.3"
32
+ },
33
+ "nx": {
34
+ "targets": {
35
+ "build": {
36
+ "outputs": [
37
+ "{projectRoot}/dist"
38
+ ],
39
+ "cache": true
40
+ }
41
+ }
42
+ },
43
+ "scripts": {
44
+ "build": "tsc -p tsconfig.build.json --noCheck",
45
+ "clean": "node -e \"require('fs').rmSync('dist',{recursive:true,force:true})\""
46
+ }
47
+ }