Two recipes for file packaging and runtime configuration - bundling project files into a zip archive and loading settings from JSON + CSV config files at startup.
Modules: zip plugin · OS · Path · Konsol
Takes a source directory and an output zip path, walks the top level, and adds every file as a zip entry. After Zip:Close writes the archive to disk, the script reopens it and lists every entry to confirm the bundle - a full create-and-verify cycle in one run.
Key patterns:
Zip:Open(path, outHandle) - create or open an archive; throws ZipException on failureZip:AddFile(handle, entryName, filePath, outOk) - add a file from disk (flushed lazily at Zip:Close)Zip:AddDir(handle, dirName, outOk) - record a directory entry so extractors recreate the folderZip:Error(handle, outMsg) - read the last error after a failed AddFileZip:Close(handle) - flush and finalize; Zip:Discard to abort without savingZip:Count(handle, outN) / Zip:Name(handle, index, outName) - inspect entries after reopeningUsage
minks zip_bundler.ks <output.zip> <source-dir>
# examples
minks zip_bundler.ks release.zip ./src
minks zip_bundler.ks assets.zip ./images
Sample output
=== Zip Bundler ===
Source : ./src
Output : release.zip
added main.ks
added utils.ks
added config.ks
dir tests/
Bundled 3 file(s) - skipped 1.
Verifying: release.zip
[1] main.ks
[2] utils.ks
[3] config.ks
[4] tests/
4 entries confirmed in release.zip.
Done.
Script
// zip_bundler.ks - bundle a directory's files into a zip archive
// Modules: zip plugin, OS, Path, Konsol
// Usage: minks zip_bundler.ks <output.zip> <source-dir>
//
// Walks the top level of <source-dir>, adds every file to <output.zip>,
// marks subdirectory names as directory entries, then reopens the archive
// to list all entries and confirm the bundle.
//
// To bundle a nested tree, call the script once per subdirectory and
// pass the same output.zip (Zip:Open on an existing archive appends).
#include "zip"
Konsol:Print("=== Zip Bundler ===");
// ── Step 1: Read arguments ────────────────────────────────────────────────────
Var:List args;
OS:Args(args);
Var:Number argc;
List:Size(args, argc);
if (argc < 2) {
Konsol:Print("Usage: minks zip_bundler.ks <output.zip> <source-dir>");
Konsol:Exit(1);
}
Var:String outZip;
Var:String srcDir;
List:Get(0, args, outZip);
List:Get(1, args, srcDir);
Var:Boolean dirExists;
Path:IsDirectory(srcDir, dirExists);
if (!dirExists) {
Konsol:Print("Not a directory: ${srcDir}");
Konsol:Exit(1);
}
Konsol:Print("Source : ${srcDir}");
Konsol:Print("Output : ${outZip}");
// ── Step 2: Open (or create) the archive ─────────────────────────────────────
// Zip:Open creates the file if it does not exist; appends if it does.
// ZipException fires when the path is not writable or the file is corrupt.
Var:Number z;
Var:Boolean ok;
try {
Zip:Open(outZip, z);
} catch (ZipException e) {
Konsol:Print("Cannot open archive: ${e.message}");
Konsol:Exit(1);
}
// ── Step 3: Walk the source directory and add entries ─────────────────────────
// OS:ListDirectory fills a List with entry names (basenames only, not full paths).
Var:List entries;
OS:ListDirectory(srcDir, entries);
Var:Number entryCount;
List:Size(entries, entryCount);
Var:Number added = 0;
Var:Number skipped = 0;
for (Number i = 0; i < entryCount; i++) {
Var:String name;
List:Get(i, entries, name);
// Reconstruct the full filesystem path for Path:IsFile.
Var:String fullPath;
Path:Join(srcDir, name, fullPath);
Var:Boolean isFile;
Path:IsFile(fullPath, isFile);
if (isFile) {
// Zip:AddFile(handle, entryName, filePath, outOk)
// entryName is the name the file gets inside the zip.
Zip:AddFile(z, name, fullPath, ok);
if (ok) {
Konsol:Print(" added ${name}");
added = added + 1;
} else {
Var:String errMsg;
Zip:Error(z, errMsg);
Konsol:Print(" SKIP ${name} (${errMsg})");
skipped = skipped + 1;
}
} else {
// Record the directory entry so extraction tools recreate the folder.
Zip:AddDir(z, name, ok);
Konsol:Print(" dir ${name}/");
}
}
// ── Step 4: Write the archive to disk ────────────────────────────────────────
// Zip:Close finalises all pending file reads and flushes to disk.
// Use Zip:Discard to abort without saving changes.
Zip:Close(z);
Konsol:Print("");
Konsol:Print("Bundled ${added} file(s) - skipped ${skipped}.");
// ── Step 5: Verify by reopening and listing entries ──────────────────────────
// Zip:Name(handle, index, outName) returns the entry name at a zero-based index.
// Zip:Count(handle, outN) gives the total number of entries.
Konsol:Print("Verifying: ${outZip}");
Zip:Open(outZip, z);
Var:Number total;
Zip:Count(z, total);
for (Number j = 0; j < total; j++) {
Var:String entryName;
Zip:Name(z, j, entryName);
Var:Number num = j + 1;
Konsol:Print(" [${num}] ${entryName}");
}
Zip:Close(z);
Konsol:Print("${total} entries confirmed in ${outZip}.");
Konsol:Print("Done.");
Modules: JSON · CSV · Dictionary · File · String · Konsol
Loads a two-layer configuration: base settings from app.json, then per-environment overrides from profiles.csv. The merged result is stored in a Dictionary so any part of your script can query a setting with a single Dictionary:Get call - no matter whether the value came from JSON or CSV.
Key patterns:
JSON:Parse + JSON:Get - pull typed values from the JSON base configCSV:Parse + CSV:Rows(name, outN) + CSV:Get(row, col, name, outStr) - iterate profile rowsDictionary:New + Dictionary:Set - assemble the merged config in one placeDictionary:Get - retrieve any setting by name in downstream codeShips with two sample files:
| File | Purpose |
|---|---|
app.json |
Base settings - env, host, port, debug, timeout, logLevel, maxConnections |
profiles.csv |
Per-environment overrides - rows for dev, staging, prod |
Usage
# use env from app.json ("dev" by default)
minks config_reader.ks
# override env from the command line
minks config_reader.ks prod
minks config_reader.ks staging
Sample output - prod profile
=== Config Reader ===
Active env: prod
--- Active Configuration ---
env : prod
host : api.example.com
port : 443
debug : false
timeout : 30s
logLevel : info
maxConnections : 500
Done.
Sample files
app.json:
{
"env": "dev",
"host": "localhost",
"port": 8080,
"debug": true,
"timeout": 30,
"logLevel": "info",
"maxConnections": 100
}
profiles.csv:
env,host,port,debug,timeout,maxConnections
dev,localhost,3000,true,5,10
staging,staging.example.com,443,false,15,50
prod,api.example.com,443,false,30,500
Script
// config_reader.ks - merge JSON base config with a CSV profile override
// Modules: JSON, CSV, Dictionary, File, String, Konsol
// Usage: minks config_reader.ks [env]
// env defaults to the "env" field in app.json
//
// Reads app.json for base settings, looks up a matching row in profiles.csv
// to override environment-specific values, then stores the merged result in
// a Dictionary so any part of your script can query it with Dictionary:Get.
//
// Sample files shipped alongside this script:
// app.json - base configuration
// profiles.csv - per-environment overrides (dev / staging / prod)
Konsol:Print("=== Config Reader ===");
// ── Step 1: Read the JSON base config ────────────────────────────────────────
// File:Open / ReadLine / EOF gives us the raw text; JSON:Parse builds a
// document we can query with dot-notation paths.
Var:Number jfh;
File:Open("app.json", "r", jfh);
Var:String jsonContent = "";
Var:String jline;
Var:Boolean jeof;
File:EOF(jfh, jeof);
while (!jeof) {
File:ReadLine(jfh, jline);
jsonContent = jsonContent + jline + "\n";
File:EOF(jfh, jeof);
}
File:Close(jfh);
JSON:Parse(jsonContent, cfg);
// Pull every base value into typed variables.
Var:String baseEnv;
Var:String baseHost;
Var:Number basePort;
Var:String baseDebug;
Var:Number baseTimeout;
Var:String baseLogLevel;
Var:Number baseMaxConn;
JSON:Get("env", cfg, baseEnv);
JSON:Get("host", cfg, baseHost);
JSON:Get("port", cfg, basePort);
JSON:Get("debug", cfg, baseDebug);
JSON:Get("timeout", cfg, baseTimeout);
JSON:Get("logLevel", cfg, baseLogLevel);
JSON:Get("maxConnections", cfg, baseMaxConn);
// ── Step 2: Determine the active environment ──────────────────────────────────
// Use the first CLI arg if provided, otherwise fall back to app.json "env".
Var:List args;
OS:Args(args);
Var:Number argc;
List:Size(args, argc);
Var:String activeEnv = baseEnv;
if (argc >= 1) {
List:Get(0, args, activeEnv);
}
Konsol:Print("Active env: ${activeEnv}");
// ── Step 3: Read the CSV profiles ────────────────────────────────────────────
// CSV:Parse builds a document; rows/cells are accessed by zero-based indices.
// Row 0 is the header row - data rows start at index 1.
Var:Number cfh;
File:Open("profiles.csv", "r", cfh);
Var:String csvContent = "";
Var:String cline;
Var:Boolean ceof;
File:EOF(cfh, ceof);
while (!ceof) {
File:ReadLine(cfh, cline);
csvContent = csvContent + cline + "\n";
File:EOF(cfh, ceof);
}
File:Close(cfh);
CSV:Parse(csvContent, profiles);
Var:Number rowCount;
CSV:Rows(profiles, rowCount);
// ── Step 4: Find and apply the matching profile row ───────────────────────────
// Column layout (from the header row):
// 0=env 1=host 2=port 3=debug 4=timeout 5=maxConnections
Var:String profHost = baseHost;
Var:String profPort = "${basePort}";
Var:String profDebug = baseDebug;
Var:String profTimeout = "${baseTimeout}";
Var:String profMaxConn = "${baseMaxConn}";
Var:Boolean profileFound = false;
for (Number i = 1; i < rowCount; i++) {
Var:String envName;
CSV:Get(i, 0, profiles, envName);
String:Trim(envName, envName);
if (envName == activeEnv) {
CSV:Get(i, 1, profiles, profHost);
CSV:Get(i, 2, profiles, profPort);
CSV:Get(i, 3, profiles, profDebug);
CSV:Get(i, 4, profiles, profTimeout);
CSV:Get(i, 5, profiles, profMaxConn);
profileFound = true;
}
}
CSV:Free(profiles);
if (!profileFound) {
Konsol:Print("No profile found for env '${activeEnv}' - using base config.");
}
// ── Step 5: Merge into a Dictionary ──────────────────────────────────────────
// Store every setting in a single Dictionary so downstream code can query any key
// with Dictionary:Get without caring whether the value came from JSON or CSV.
Dictionary:New config;
Dictionary:Set("env", activeEnv, config);
Dictionary:Set("host", profHost, config);
Dictionary:Set("port", profPort, config);
Dictionary:Set("debug", profDebug, config);
Dictionary:Set("timeout", profTimeout, config);
Dictionary:Set("logLevel", baseLogLevel, config);
Dictionary:Set("maxConnections", profMaxConn, config);
// ── Step 6: Print the active configuration ────────────────────────────────────
Konsol:Print("");
Konsol:Print("--- Active Configuration ---");
Var:String val;
Dictionary:Get("env", config, val); Konsol:Print("env : ${val}");
Dictionary:Get("host", config, val); Konsol:Print("host : ${val}");
Dictionary:Get("port", config, val); Konsol:Print("port : ${val}");
Dictionary:Get("debug", config, val); Konsol:Print("debug : ${val}");
Dictionary:Get("timeout", config, val); Konsol:Print("timeout : ${val}s");
Dictionary:Get("logLevel", config, val); Konsol:Print("logLevel : ${val}");
Dictionary:Get("maxConnections", config, val); Konsol:Print("maxConnections : ${val}");
Konsol:Print("Done.");