kz3-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 ADDED
@@ -0,0 +1,98 @@
1
+ # kz3-cli
2
+
3
+ ## Usage
4
+
5
+ ```txt
6
+
7
+ USAGE kz3 [OPTIONS] preview
8
+
9
+ GLOBAL OPTIONS
10
+ --cwd Current working directory to resolve config and output from
11
+ -c, --configFile="kz.config" Path to kz config file (e.g. kz.config, kz3.config.ts)
12
+ --catalogUrl URL to the catalog JSON file
13
+ --debug Enable debug logging
14
+ --repo Catalog ID
15
+
16
+ COMMANDS
17
+ preview
18
+ preview dlp
19
+ ```
20
+
21
+ ## Commands
22
+
23
+ ### `preview`
24
+
25
+ The `preview` command has sub-commands to download the package for preview.
26
+
27
+ **Usage:**
28
+
29
+ ```bash
30
+ kz3-cli preview dlp [OPTIONS] <APPID>
31
+ ```
32
+
33
+ ### Example: Basic
34
+
35
+ ```bash
36
+ kz3-cli preview dlp --repo repo-id app-id
37
+ ```
38
+
39
+ The example command assumes that we have a `kz.config.ts` file in the current working directory with the following content:
40
+
41
+ ```ts
42
+ import { defineConfig } from "kz3-cli";
43
+ export default defineConfig({
44
+ registries: {
45
+ "repo-id": "http://localhost:3000/catalog.json",
46
+ },
47
+ });
48
+ ```
49
+
50
+ ### Example: Multiple Registries
51
+
52
+ ```bash
53
+ # `app-id` will be downloaded from the `github-1`
54
+ kz3-cli preview dlp --repo github-1 app-id
55
+
56
+ # `app-id` will be downloaded from the `repo-id`
57
+ kz3-cli preview dlp --repo repo-id app-id
58
+ ```
59
+
60
+ **kz.config.ts:**
61
+
62
+ ```ts
63
+ import { defineConfig } from "kz3-cli";
64
+ export default defineConfig({
65
+ defaultRepo: "repo-id",
66
+ registries: {
67
+ "repo-id": "http://localhost:3000/catalog.json",
68
+ "github-1": "https://raw.githubusercontent.com/username/repo/branch/catalog.json",
69
+ },
70
+ });
71
+ ```
72
+
73
+ ## Catalog File
74
+
75
+ The catalog file is a JSON file that contains the metadata of the packages. The structure of the catalog file is as follows:
76
+
77
+ ```json
78
+ [
79
+ {
80
+ "appName": "app-id",
81
+ "version": "1.0.0",
82
+ "url": "http://localhost:3000/app-id-1.0.0.tar.gz",
83
+ "provider": "http"
84
+ },
85
+ {
86
+ "appName": "app-id",
87
+ "version": "1.0.0",
88
+ "url": "https://gitlab.com/....../app-id-1.0.0.tar.gz",
89
+ "provider": "gitlab"
90
+ }
91
+ ]
92
+ ```
93
+
94
+ ## Auth and Providers
95
+
96
+ Only supported is HTTP provider and Gitlab provider.
97
+
98
+ For Gitlab, the environment variable `KZ_AUTH` must be present for the cli to download the package. The value of `KZ_AUTH` should be a Gitlab Deploy Token that can read packages and files in the repository.
@@ -0,0 +1,34 @@
1
+ //#region src/globalArgs.ts
2
+ const dryRunArg = { dryRun: {
3
+ type: "boolean",
4
+ description: "Print the download URL without downloading"
5
+ } };
6
+ const globalArgs = {
7
+ cwd: {
8
+ type: "string",
9
+ description: "Current working directory to resolve config and output from"
10
+ },
11
+ configFile: {
12
+ type: "string",
13
+ alias: ["c"],
14
+ description: "Path to kz config file (e.g. kz.config, kz3.config.ts)",
15
+ default: "kz.config",
16
+ required: false
17
+ },
18
+ catalogUrl: {
19
+ type: "string",
20
+ description: "URL to the catalog JSON file",
21
+ required: false
22
+ },
23
+ debug: {
24
+ type: "boolean",
25
+ description: "Enable debug logging"
26
+ },
27
+ repo: {
28
+ type: "string",
29
+ description: "Catalog ID",
30
+ required: false
31
+ }
32
+ };
33
+ //#endregion
34
+ export { globalArgs as n, dryRunArg as t };
@@ -0,0 +1,14 @@
1
+ import { C12InputConfig, LoadConfigOptions } from "c12";
2
+
3
+ //#region src/util/load-kz-config.d.ts
4
+ interface Kz3Config {
5
+ defaultRepo?: string;
6
+ registries: Record<string, string>;
7
+ }
8
+ interface Kz3LoadConfigOptions extends Pick<LoadConfigOptions, "configFile" | "cwd"> {}
9
+ type Kz3DefineConfigInput = (Kz3Config & {
10
+ extends?: never;
11
+ }) & C12InputConfig<Kz3Config>;
12
+ declare const defineConfig: (input: Kz3DefineConfigInput) => Kz3DefineConfigInput;
13
+ //#endregion
14
+ export { type Kz3Config, type Kz3DefineConfigInput, type Kz3LoadConfigOptions, defineConfig };
package/dist/index.mjs ADDED
@@ -0,0 +1,2 @@
1
+ import { t as defineConfig } from "./load-kz-config-CMLz3_Tq.mjs";
2
+ export { defineConfig };
@@ -0,0 +1 @@
1
+ export { };
@@ -0,0 +1,19 @@
1
+ import { n as globalArgs } from "./globalArgs-DdR7vgA6.mjs";
2
+ import { defineCommand, runMain } from "citty";
3
+ //#region bin/index.ts
4
+ const main = defineCommand({
5
+ meta: {
6
+ name: "kz3",
7
+ description: "KZ3 CLI"
8
+ },
9
+ subCommands: { preview: () => import("./main-DLy6TSIK.mjs").then((m) => m.default) },
10
+ args: { ...globalArgs }
11
+ });
12
+ try {
13
+ await runMain(main);
14
+ } catch (error) {
15
+ console.error("An error occurred:", error instanceof Error ? error.message : error);
16
+ process.exit(1);
17
+ }
18
+ //#endregion
19
+ export {};
@@ -0,0 +1,24 @@
1
+ import { loadConfig } from "c12";
2
+ //#region src/util/load-kz-config.ts
3
+ async function loadKz3Config(options) {
4
+ const { config } = await loadConfig({
5
+ name: "kz",
6
+ dotenv: true,
7
+ ...options
8
+ });
9
+ function getDefaultRepo() {
10
+ if (config.registries && Object.keys(config.registries).length === 1) return Object.values(config.registries)[0];
11
+ if (config.defaultRepo) if (config.registries && config.registries[config.defaultRepo]) return config.registries[config.defaultRepo];
12
+ else {
13
+ console.warn(`Warning: defaultRepo "${config.defaultRepo}" is not found in registries. Please check your config.`);
14
+ return null;
15
+ }
16
+ }
17
+ return {
18
+ ...config,
19
+ getDefaultRepo
20
+ };
21
+ }
22
+ const defineConfig = (input) => input;
23
+ //#endregion
24
+ export { loadKz3Config as n, defineConfig as t };
@@ -0,0 +1,257 @@
1
+ import { n as loadKz3Config } from "./load-kz-config-CMLz3_Tq.mjs";
2
+ import { n as globalArgs, t as dryRunArg } from "./globalArgs-DdR7vgA6.mjs";
3
+ import { defineCommand } from "citty";
4
+ import path, { relative } from "node:path";
5
+ import fs, { mkdir } from "node:fs/promises";
6
+ import fs$1, { existsSync } from "node:fs";
7
+ import { pipeline } from "node:stream";
8
+ import { promisify } from "node:util";
9
+ import { extract } from "tar";
10
+ import "valid-filename";
11
+ import ejs from "ejs";
12
+ import { fileURLToPath } from "node:url";
13
+ //#region src/preview-module/core/providers.ts
14
+ /**
15
+ * Generic HTTP, with or without Basic Auth.
16
+ *
17
+ */
18
+ const http = (url, options) => {
19
+ const requestHeaders = {};
20
+ const originalUrl = url;
21
+ let transformedUrl = url;
22
+ if (options.auth) requestHeaders["Authorization"] = `Basic ${options.auth}`;
23
+ if (options.token) transformedUrl = url.replace(":token", options.token);
24
+ return {
25
+ headers: requestHeaders,
26
+ originalUrl,
27
+ url: transformedUrl
28
+ };
29
+ };
30
+ const gitlab = (url, options) => {
31
+ return {
32
+ url,
33
+ headers: { authorization: options.auth ? `Bearer ${options.auth}` : void 0 }
34
+ };
35
+ };
36
+ const providers = {
37
+ http,
38
+ gitlab
39
+ };
40
+ //#endregion
41
+ //#region src/util/download.ts
42
+ async function download(url, outFile, options = {}) {
43
+ const response = await sendDownloadRequest(url, { headers: options.requestHeaders });
44
+ if (response.status >= 400) throw new Error(`Failed to download from ${url}: HTTP ${response.status} ${response.statusText}`);
45
+ const outputStream = fs$1.createWriteStream(outFile);
46
+ await promisify(pipeline)(response.body, outputStream);
47
+ }
48
+ async function sendDownloadRequest(url, options) {
49
+ const result = await fetch(url, options).catch((error) => {
50
+ throw new Error(`Failed to download from ${url}: ${error.message}`, { cause: error });
51
+ });
52
+ if (result.status >= 400) throw new Error(`Failed to download from ${url}: HTTP ${result.status} ${result.statusText}`);
53
+ return result;
54
+ }
55
+ //#endregion
56
+ //#region src/util/common.ts
57
+ function toSafeFsName(str) {
58
+ return str.replace(/[^\da-z-.]/gi, "-");
59
+ }
60
+ /**
61
+ * Fixes a template filename by replacing certain characters to comply with templating.
62
+ *
63
+ * - `_` becomes `.` (for dotfiles and folder names like `_git` -> `.git`)
64
+ * - `__` becomes literal `_` (used for escaping)
65
+ * - `.tmpl` extension is removed (used for template files that should not be rendered as-is for certain file extensions)
66
+ *
67
+ * @param filename filename or path-like
68
+ */
69
+ function fixTemplateFilename(filename) {
70
+ const tmplFilesMap = {
71
+ ".ts.tmpl": ".ts",
72
+ ".js.tmpl": ".js"
73
+ };
74
+ const dots = (str) => {
75
+ return str.split(path.sep).map((part) => {
76
+ if (part.startsWith("__")) return part.replace(/^__/, "_");
77
+ else if (part.startsWith("_")) return part.replace(/^_/, ".");
78
+ return part;
79
+ }).join(path.sep);
80
+ };
81
+ const ext = Object.keys(tmplFilesMap).find((ext) => filename.endsWith(ext));
82
+ if (ext) return dots(filename.replace(ext, tmplFilesMap[ext]));
83
+ return dots(filename);
84
+ }
85
+ //#endregion
86
+ //#region src/preview-module/core/download-dist.ts
87
+ async function downloadDistPackage(options) {
88
+ const { dryRun } = options;
89
+ const providerName = options.provider;
90
+ if (!providerName) throw new Error("Provider is required to download dist package");
91
+ const getProvider = providers[providerName];
92
+ if (!getProvider) throw new Error(`Unknown provider: ${providerName}`);
93
+ options.auth = process.env.KZ3_AUTH ?? options.auth;
94
+ const cwd = options.cwd || process.cwd();
95
+ const provider = await getProvider(options.registry, { auth: options.auth });
96
+ const appName = toSafeFsName(options.appName);
97
+ const tempDir = path.resolve(cwd, ".temp", appName);
98
+ const tarPath = path.resolve(tempDir, appName + ".tar.gz");
99
+ if (dryRun) console.log(`[Dry Run] mkdir ${path.dirname(tarPath)}`);
100
+ else await mkdir(path.dirname(tarPath), { recursive: true });
101
+ const startTime = Date.now();
102
+ if (dryRun) {
103
+ console.log(`[Dry Run] Downloading from ${provider.url} to ${tarPath}`);
104
+ console.log(`[Dry Run] With headers: ${JSON.stringify(provider.headers)}`);
105
+ } else await download(provider.url, tarPath, { requestHeaders: { ...provider.headers } }).catch((error) => {
106
+ if (!existsSync(tarPath)) throw error;
107
+ throw new Error(`Failed to download package`, { cause: error });
108
+ });
109
+ const durationMs = Date.now() - startTime;
110
+ console.debug(`Downloaded dist package in ${durationMs}ms`);
111
+ if (!existsSync(tarPath) && !dryRun) throw new Error(`Failed to download package: file not found at ${tarPath}`);
112
+ const extractDir = path.resolve(cwd, "apps", appName, "dist");
113
+ if (dryRun) console.log(`[Dry Run] Extracting ${relative(cwd, tarPath)} to ${relative(cwd, extractDir)}`);
114
+ else {
115
+ try {
116
+ await mkdir(extractDir, { recursive: true });
117
+ await extract({
118
+ file: tarPath,
119
+ cwd: extractDir,
120
+ strip: 1
121
+ });
122
+ } catch (error) {
123
+ throw new Error(`Failed to extract package`, { cause: error });
124
+ }
125
+ console.log(`Extracted package to ${extractDir}`);
126
+ }
127
+ return {
128
+ outDir: extractDir,
129
+ tempPackageFile: tarPath,
130
+ tempPackageDir: tempDir,
131
+ url: provider.url
132
+ };
133
+ }
134
+ //#endregion
135
+ //#region src/preview-module/core/catalog.ts
136
+ /**
137
+ * Loads the catalog from the given URL.
138
+ * The catalog is expected to be a JSON
139
+ */
140
+ async function loadCatalogData(options) {
141
+ if (!options.url) throw new Error("Catalog URL is required to load catalog data");
142
+ const response = await fetch(options.url, {
143
+ method: "GET",
144
+ headers: { Accept: "application/json" }
145
+ });
146
+ if (response.status >= 400) throw new Error(`Failed to load catalog from ${options.url}: HTTP ${response.status} ${response.statusText}`);
147
+ const data = await response.json().catch((error) => {
148
+ throw new Error(`Failed to parse catalog JSON from ${options.url}: ${error.message}`, { cause: error });
149
+ });
150
+ if (!Array.isArray(data)) throw new Error(`Invalid catalog format from ${options.url}: expected an array of entries`);
151
+ return data;
152
+ }
153
+ async function getCatalogEntryByAppName(catalog, appName) {
154
+ const entry = catalog.find((entry) => entry.appName === appName);
155
+ if (!entry) throw new Error(`App "${appName}" was not found in the catalog`);
156
+ if (!entry.url) throw new Error(`Catalog entry for "${appName}" has no download URL`);
157
+ if (!providers[entry.provider]) throw new Error(`Unknown provider "${entry.provider}" for app "${appName}"`);
158
+ return entry;
159
+ }
160
+ //#endregion
161
+ //#region src/preview-module/core/create-app.ts
162
+ /**
163
+ * Creates the files from a template.
164
+ */
165
+ async function createApp(options) {
166
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
167
+ const templatePath = path.resolve(__dirname, "../template");
168
+ const renderTemplatePath = path.resolve(templatePath, "render");
169
+ const staticTemplatePath = path.resolve(templatePath, "static");
170
+ const appDir = path.resolve(options.cwd, "apps", options.appName);
171
+ if (!existsSync(templatePath)) {
172
+ console.error(`Template path does not exist: ${templatePath}`);
173
+ throw new Error(`Unable to retrieve template`);
174
+ }
175
+ const vfs = {};
176
+ const packageJsonTemplate = await fs.readFile(path.join(renderTemplatePath, "package.json"), "utf-8");
177
+ vfs["package.json"] = await ejs.render(packageJsonTemplate, { appName: options.appName }, { async: true });
178
+ console.log(`App Dir: ${appDir}`);
179
+ for (const [file, content] of Object.entries(vfs)) {
180
+ const filePath = path.join(appDir, file);
181
+ if (options.dryRun) {
182
+ console.log(`[Dry Run] Writing file ${path.relative(process.cwd(), filePath)}`);
183
+ continue;
184
+ } else {
185
+ await fs.mkdir(path.dirname(filePath), { recursive: true });
186
+ await fs.writeFile(filePath, content, "utf-8");
187
+ }
188
+ }
189
+ const staticFiles = await fs.readdir(staticTemplatePath, { recursive: true });
190
+ for (const file of staticFiles) {
191
+ const staticFileDest = fixTemplateFilename(file);
192
+ if (options.dryRun) console.log(`[Dry Run] Copying static file from ${path.join(staticTemplatePath, file)} to ${path.join(appDir, staticFileDest)}`);
193
+ else await fs.copyFile(path.join(staticTemplatePath, file), path.join(appDir, staticFileDest));
194
+ }
195
+ return { outDir: appDir };
196
+ }
197
+ //#endregion
198
+ //#region src/preview-module/main.ts
199
+ const previewModule = defineCommand({
200
+ meta: { name: "preview" },
201
+ subCommands: { dlp: defineCommand({
202
+ meta: {
203
+ name: "dlp",
204
+ description: "Download a preview distribution package"
205
+ },
206
+ args: {
207
+ appId: {
208
+ type: "positional",
209
+ required: true,
210
+ description: "Name of the app to download distribution for"
211
+ },
212
+ ...dryRunArg,
213
+ ...globalArgs
214
+ },
215
+ async run({ args }) {
216
+ args.debug && console.log(args);
217
+ const cwd = args.cwd || process.cwd();
218
+ const repo = args.repo;
219
+ const kzConfig = await loadKz3Config({
220
+ configFile: args.configFile,
221
+ cwd
222
+ });
223
+ const repoKey = kzConfig?.registries && Object.keys(kzConfig.registries).find((key) => key === repo);
224
+ const catalogFromConfig = repoKey ? kzConfig.registries[repoKey] : null;
225
+ if (!(args.catalogUrl || catalogFromConfig || kzConfig.getDefaultRepo())) {
226
+ console.error(`Error: Catalog URL not specified.`);
227
+ console.error(`Please provide a catalog URL using the "--catalog-url" argument, or define it in the kz config under registries then use --repo to specify which one to use.`);
228
+ process.exit(1);
229
+ }
230
+ const catalog = await loadCatalogData({ url: args.catalogUrl });
231
+ if (catalog.length === 0) {
232
+ console.log("No entries found in the catalog");
233
+ process.exit(0);
234
+ }
235
+ const app = await getCatalogEntryByAppName(catalog, args.appId);
236
+ const downloadResult = await downloadDistPackage({
237
+ appName: app.appName,
238
+ registry: app.url,
239
+ provider: app.provider,
240
+ configFile: args.configFile,
241
+ dryRun: false,
242
+ cwd,
243
+ tempDir: ".temp"
244
+ });
245
+ console.log(`Package for "${app.appName}:${app.version}" downloaded to ${downloadResult.outDir}`);
246
+ const createAppResult = await createApp({
247
+ appName: app.appName,
248
+ cwd,
249
+ configFile: args.configFile,
250
+ dryRun: args.dryRun
251
+ });
252
+ console.log(`App config created at ${createAppResult.outDir}`);
253
+ }
254
+ }) }
255
+ });
256
+ //#endregion
257
+ export { previewModule as default };
package/package.json ADDED
@@ -0,0 +1,53 @@
1
+ {
2
+ "name": "kz3-cli",
3
+ "version": "0.1.0",
4
+ "description": "kz3-cli",
5
+ "homepage": "https://github.com/hirameki-labs/discussion",
6
+ "bugs": {
7
+ "url": "https://github.com/hirameki-labs/discussion/issues"
8
+ },
9
+ "license": "MIT",
10
+ "author": "Mark Terence Tiglao <github@markterence.me>",
11
+ "repository": {
12
+ "type": "git",
13
+ "url": "git+https://github.com/hirameki-labs/discussion.git"
14
+ },
15
+ "bin": {
16
+ "kz3": "./dist/kz3-cli.mjs"
17
+ },
18
+ "files": [
19
+ "dist"
20
+ ],
21
+ "type": "module",
22
+ "exports": {
23
+ ".": "./dist/index.mjs",
24
+ "./kz3-cli": "./dist/kz3-cli.mjs",
25
+ "./package.json": "./package.json"
26
+ },
27
+ "publishConfig": {
28
+ "access": "public"
29
+ },
30
+ "dependencies": {
31
+ "@dotenvx/dotenvx": "^1.57.2",
32
+ "axios": "^1.13.6",
33
+ "c12": "4.0.0-beta.4",
34
+ "citty": "^0.2.1",
35
+ "ejs": "^5.0.1",
36
+ "tar": "^7.5.13",
37
+ "valid-filename": "^4.0.0"
38
+ },
39
+ "devDependencies": {
40
+ "@types/ejs": "^3.1.5",
41
+ "@types/node": "^24.12.0",
42
+ "@typescript/native-preview": "7.0.0-dev.20260316.1",
43
+ "bumpp": "^11.0.1",
44
+ "typescript": "^5.9.3",
45
+ "vite-plus": "^0.1.11"
46
+ },
47
+ "scripts": {
48
+ "build": "vp pack",
49
+ "dev": "vp pack --watch",
50
+ "test": "vp test",
51
+ "check": "vp check"
52
+ }
53
+ }