c8y-nitro 0.7.0 → 0.7.2

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/dist/index.d.mts CHANGED
@@ -1,4 +1,4 @@
1
- import { n as C8yNitroModuleOptions } from "./index-ByDSMEvU.mjs";
1
+ import { n as C8yNitroModuleOptions } from "./index-vu4xaFkz.mjs";
2
2
  import { t as TenantCredentials } from "./credentials-eKnJ7m9Y.mjs";
3
3
  import { NitroModule } from "nitro/types";
4
4
  //#region src/index.d.ts
package/dist/index.mjs CHANGED
@@ -1,8 +1,8 @@
1
- import { _ as GENERATED_READINESS_ROUTE, a as findMicroserviceByName, d as updateMicroservice, g as GENERATED_LIVENESS_ROUTE, h as GENERATED_INVALIDATE_TENANT_OPTIONS_ROUTE, l as subscribeToApplication, m as createC8yManifestFromNitro, n as createBasicAuthHeader, o as getBootstrapCredentials, p as createC8yManifest, r as createMicroservice } from "./c8y-api-BFjK6AYb.mjs";
2
- import { t as writeBootstrapCredentials } from "./env-file-BrOTmVjA.mjs";
3
- import { n as name } from "./package-BhFLRPOb.mjs";
1
+ import { _ as GENERATED_READINESS_ROUTE, g as GENERATED_LIVENESS_ROUTE, h as GENERATED_INVALIDATE_TENANT_OPTIONS_ROUTE, m as createC8yManifestFromNitro, p as createC8yManifest } from "./c8y-api-DscUs-Kd.mjs";
2
+ import { t as autoBootstrap } from "./autoBootstrap-Drk7ltXK.mjs";
3
+ import { n as name } from "./package-BHOzdhHp.mjs";
4
4
  import { basename, dirname, join, relative } from "node:path";
5
- import { mkdir, readFile, writeFile } from "node:fs/promises";
5
+ import { mkdir, writeFile } from "node:fs/promises";
6
6
  import { x } from "tinyexec";
7
7
  import { join as join$1 } from "pathe";
8
8
  import { mkdir as mkdir$1, readFile as readFile$1, stat, writeFile as writeFile$1 } from "fs/promises";
@@ -11,8 +11,6 @@ import Spinnies from "spinnies";
11
11
  import { colors } from "consola/utils";
12
12
  import { mkdirSync, writeFileSync } from "fs";
13
13
  import { fileURLToPath } from "node:url";
14
- import { createHash } from "node:crypto";
15
- import process from "node:process";
16
14
  import evlog from "evlog/nitro/v3";
17
15
  //#region src/module/apiClient.ts
18
16
  /**
@@ -220,14 +218,15 @@ async function writeAPIClient(nitro, options) {
220
218
  /**
221
219
  * Generate the Dockerfile content for a Nitro build
222
220
  * @param outputDirName - Name of the output directory (e.g., '.output')
221
+ * @param options - Dockerfile customization (base image, extra instructions)
223
222
  * @returns Dockerfile content as a string
224
223
  */
225
- function getDockerfileContent(outputDirName) {
226
- return `FROM node:24-slim AS runtime
224
+ function getDockerfileContent(outputDirName, options = {}) {
225
+ return `FROM ${options.baseImage ?? "node:24-slim"} AS runtime
227
226
 
228
227
  WORKDIR /app
229
228
 
230
- # Copy the Nitro build output
229
+ ${options.extraInstructions?.length ? `${options.extraInstructions.join("\n")}\n\n` : ""}# Copy the Nitro build output
231
230
  COPY ${outputDirName}/ ${outputDirName}/
232
231
 
233
232
  ENV NODE_ENV=production
@@ -248,12 +247,12 @@ async function checkDockerInstalled() {
248
247
  return false;
249
248
  }
250
249
  }
251
- async function writeDockerfile(outputDir) {
250
+ async function writeDockerfile(outputDir, options) {
252
251
  const outputDirName = basename(outputDir);
253
252
  const c8yDir = join(outputDir, "../.c8y");
254
253
  const dockerfilePath = join(c8yDir, "Dockerfile");
255
254
  await mkdir(c8yDir, { recursive: true });
256
- await writeFile(dockerfilePath, getDockerfileContent(outputDirName), "utf-8");
255
+ await writeFile(dockerfilePath, getDockerfileContent(outputDirName, options), "utf-8");
257
256
  return c8yDir;
258
257
  }
259
258
  async function buildDockerImage(nitro, c8yDir) {
@@ -302,12 +301,13 @@ async function saveDockerImageToTar(nitro, c8yDir, imageName) {
302
301
  /**
303
302
  * Create a Docker image from the Nitro build output
304
303
  * @param nitro Nitro instance
304
+ * @param options Dockerfile customization (base image, extra instructions)
305
305
  * @returns Path to the saved Docker image tar file
306
306
  */
307
- async function createDockerImage(nitro) {
307
+ async function createDockerImage(nitro, options = {}) {
308
308
  if (!await checkDockerInstalled()) throw new Error("Docker is not installed or not available in PATH. Please install Docker to build images.");
309
309
  nitro.logger.debug("Creating Docker image...");
310
- const c8yDir = await writeDockerfile(nitro.options.output.dir);
310
+ const c8yDir = await writeDockerfile(nitro.options.output.dir, options);
311
311
  const imageTarPath = await saveDockerImageToTar(nitro, c8yDir, await buildDockerImage(nitro, c8yDir));
312
312
  nitro.logger.debug("Docker image creation complete.");
313
313
  return imageTarPath;
@@ -337,12 +337,12 @@ function formatBytes(bytes) {
337
337
  function resolveZipOutputPath(rootDir, options, manifest) {
338
338
  return join$1(join$1(rootDir, options.outputDir ?? "./"), typeof options.name === "function" ? options.name(manifest.name, manifest.version) : options.name ?? `${manifest.name}-${manifest.version}.zip`);
339
339
  }
340
- async function createC8yZip(nitro, options = {}) {
340
+ async function createC8yZip(nitro, options = {}, dockerOptions = {}) {
341
341
  const startTime = Date.now();
342
342
  const spinnerName = "c8y-zip";
343
343
  spinnies.add(spinnerName, { text: "Creating Dockerfile..." });
344
344
  spinnies.update(spinnerName, { text: "Building Docker image..." });
345
- const imageTarPath = await createDockerImage(nitro);
345
+ const imageTarPath = await createDockerImage(nitro, dockerOptions);
346
346
  spinnies.update(spinnerName, { text: "Creating manifest..." });
347
347
  const manifest = await createC8yManifestFromNitro(nitro);
348
348
  spinnies.update(spinnerName, { text: "Reading image.tar..." });
@@ -421,6 +421,9 @@ async function setupRuntimeConfig(nitro, options) {
421
421
  nitro.options.runtimeConfig.c8yTenantOptionsPerKeyTTL = options.cache?.tenantOptions ?? {};
422
422
  const manifest = await createC8yManifest(nitro.options.rootDir, nitro.options, nitro.logger);
423
423
  nitro.options.runtimeConfig.c8ySettingsCategory = options.manifest?.settingsCategory ?? manifest.contextPath ?? manifest.name;
424
+ const excludeInternalRoutes = options.openapi?.excludeInternalRoutes ?? true;
425
+ const probePaths = [options.manifest?.livenessProbe?.httpGet?.path, options.manifest?.readinessProbe?.httpGet?.path].filter((path) => Boolean(path));
426
+ nitro.options.runtimeConfig.c8yOpenApiExcludeRoutes = [...excludeInternalRoutes ? ["/_", ...probePaths] : [], ...options.openapi?.excludeRoutes ?? []];
424
427
  }
425
428
  //#endregion
426
429
  //#region src/module/registerRuntime.ts
@@ -461,6 +464,31 @@ function registerRuntime(nitro, options = {}) {
461
464
  middleware: true
462
465
  })));
463
466
  /**
467
+ * Request URL rewrite middleware (always on in production builds).
468
+ * Rewrites `event.url` to the public tenant endpoint so handlers that build
469
+ * absolute URLs from the request don't leak internal cluster URLs.
470
+ * Registered after the scanned middlewares (which observe the original
471
+ * cluster-local URL) and before the OpenAPI transform, which reuses the
472
+ * resolved tenant domain from its cache.
473
+ */
474
+ if (!isNitroDev) {
475
+ nitro.options.handlers.push({
476
+ route: "/**",
477
+ handler: join$1(thisFilePath, "./runtime/handlers/rewriteRequestUrl"),
478
+ middleware: true
479
+ });
480
+ nitro.logger.debug("Registered public request URL rewrite middleware");
481
+ }
482
+ if (Boolean(nitro.options.experimental?.openAPI && (isNitroDev || nitro.options.openAPI?.production))) if (!isNitroDev && nitro.options.openAPI?.production === "prerender") nitro.logger.warn("c8y-nitro cannot transform a prerendered OpenAPI document (internal routes stay listed and the server URL is baked in at build time). Use `openAPI: { production: true }` to serve the document at runtime instead.");
483
+ else {
484
+ nitro.options.handlers.push({
485
+ route: "/**",
486
+ handler: join$1(thisFilePath, "./runtime/handlers/openapi"),
487
+ middleware: true
488
+ });
489
+ nitro.logger.debug(`Registered OpenAPI transform middleware for ${nitro.options.openAPI?.route || "/_openapi.json"}`);
490
+ }
491
+ /**
464
492
  * Handlers (can't be auto scanned as they need methods etc)
465
493
  */
466
494
  const handlers = [];
@@ -521,173 +549,6 @@ function checkProbeEndpoint(nitro, probe, probeType) {
521
549
  }
522
550
  }
523
551
  //#endregion
524
- //#region src/module/manifestCache.ts
525
- /**
526
- * Directory (inside the project's `node_modules`) where c8y-nitro persists
527
- * local dev state such as the last-synced manifest hash. Lives in
528
- * `node_modules` so it is naturally gitignored and scoped per-checkout.
529
- */
530
- const CACHE_DIR_SEGMENTS = ["node_modules", ".c8y-nitro"];
531
- const CACHE_FILE_NAME = "bootstrap-state.json";
532
- const CACHE_SCHEMA_VERSION = 1;
533
- function getCacheDir(rootDir) {
534
- return join(rootDir, ...CACHE_DIR_SEGMENTS);
535
- }
536
- function getCacheFile(rootDir) {
537
- return join(getCacheDir(rootDir), CACHE_FILE_NAME);
538
- }
539
- /**
540
- * Compute a hash over the manifest and the deployment target it will be synced
541
- * to. Folding the target in means switching tenant/baseURL invalidates the
542
- * cached hash even if the manifest itself is unchanged.
543
- *
544
- * @param manifest - The generated Cumulocity manifest
545
- * @param target - The deployment target identity
546
- */
547
- function hashManifest(manifest, target) {
548
- const payload = JSON.stringify({
549
- manifest,
550
- target
551
- });
552
- return createHash("sha256").update(payload).digest("hex");
553
- }
554
- /**
555
- * Read the persisted bootstrap state, or `null` when absent, unreadable, or
556
- * from an incompatible schema version.
557
- * @param rootDir - Project root directory (containing `node_modules`)
558
- */
559
- async function readManifestCache(rootDir) {
560
- try {
561
- const content = await readFile(getCacheFile(rootDir), "utf-8");
562
- const state = JSON.parse(content);
563
- if (state.schemaVersion !== CACHE_SCHEMA_VERSION) return null;
564
- return state;
565
- } catch {
566
- return null;
567
- }
568
- }
569
- /**
570
- * Persist the bootstrap state for future dev restarts.
571
- * @param rootDir - Project root directory (containing `node_modules`)
572
- * @param state - The state to persist (schema version is filled in automatically)
573
- */
574
- async function writeManifestCache(rootDir, state) {
575
- await mkdir(getCacheDir(rootDir), { recursive: true });
576
- const payload = {
577
- schemaVersion: CACHE_SCHEMA_VERSION,
578
- ...state
579
- };
580
- await writeFile(getCacheFile(rootDir), JSON.stringify(payload, null, 2), "utf-8");
581
- }
582
- //#endregion
583
- //#region src/module/autoBootstrap.ts
584
- const BOOTSTRAP_ENV_VARS = [
585
- "C8Y_BOOTSTRAP_TENANT",
586
- "C8Y_BOOTSTRAP_USER",
587
- "C8Y_BOOTSTRAP_PASSWORD"
588
- ];
589
- const DEV_ENV_VARS = [
590
- "C8Y_BASEURL",
591
- "C8Y_DEVELOPMENT_TENANT",
592
- "C8Y_DEVELOPMENT_USER",
593
- "C8Y_DEVELOPMENT_PASSWORD"
594
- ];
595
- /**
596
- * Verifies the microservice on the development tenant and keeps it in sync with
597
- * the local manifest, bootstrapping (or re-bootstrapping) when needed.
598
- *
599
- * Unlike a purely env-var-based check, this always verifies against the tenant
600
- * when development credentials are available, so it also handles:
601
- * - the manifest changing between dev restarts (auto-updates a placeholder), and
602
- * - the application being deleted on the tenant while stale bootstrap env vars
603
- * still linger locally (re-creates it).
604
- *
605
- * A previously *deployed* microservice (one with an active version) is never
606
- * overwritten automatically - we only warn about the drift.
607
- *
608
- * Runs silently unless a bootstrap/sync was performed or an error occurs.
609
- * @param nitro - Nitro instance
610
- */
611
- async function autoBootstrap(nitro) {
612
- try {
613
- const rootDir = nitro.options.rootDir;
614
- if (DEV_ENV_VARS.filter((v) => !process.env[v]).length > 0) return;
615
- const baseUrl = process.env.C8Y_BASEURL.endsWith("/") ? process.env.C8Y_BASEURL.slice(0, -1) : process.env.C8Y_BASEURL;
616
- const developmentTenant = process.env.C8Y_DEVELOPMENT_TENANT;
617
- const authHeader = createBasicAuthHeader(developmentTenant, process.env.C8Y_DEVELOPMENT_USER, process.env.C8Y_DEVELOPMENT_PASSWORD);
618
- const manifest = await createC8yManifest(rootDir, nitro.options, nitro.logger);
619
- const target = {
620
- baseUrl,
621
- developmentTenant,
622
- name: manifest.name
623
- };
624
- const currentHash = hashManifest(manifest, target);
625
- const existingApp = await findMicroserviceByName(baseUrl, manifest.name, authHeader);
626
- if (!existingApp) {
627
- const createdApp = await createMicroservice(baseUrl, manifest, authHeader);
628
- nitro.logger.debug(`Microservice "${manifest.name}" created (ID: ${createdApp.id})`);
629
- await finalizeBootstrap(nitro, {
630
- baseUrl,
631
- developmentTenant,
632
- appId: createdApp.id,
633
- authHeader,
634
- target,
635
- manifestHash: currentHash,
636
- forceCredentials: true
637
- });
638
- nitro.logger.success(`Auto-bootstrap complete! Microservice "${manifest.name}" created on tenant.`);
639
- return;
640
- }
641
- const appId = existingApp.id;
642
- const isRealService = Boolean(existingApp.activeVersionId);
643
- const cache = await readManifestCache(rootDir);
644
- if (!cache || cache.manifestHash !== currentHash || cache.appId !== appId) if (isRealService) nitro.logger.warn(`Local manifest for "${manifest.name}" differs from the deployed microservice on the development tenant. The running service was left untouched. Redeploy or run \`npx c8y-nitro bootstrap\` to update it explicitly.`);
645
- else {
646
- await updateMicroservice(baseUrl, appId, manifest, authHeader);
647
- nitro.logger.success(`Auto-bootstrap: updated microservice "${manifest.name}" with changed manifest.`);
648
- }
649
- const hasBootstrapCreds = BOOTSTRAP_ENV_VARS.every((v) => process.env[v]);
650
- const appChanged = cache?.appId !== appId;
651
- await finalizeBootstrap(nitro, {
652
- baseUrl,
653
- developmentTenant,
654
- appId,
655
- authHeader,
656
- target,
657
- manifestHash: currentHash,
658
- forceCredentials: !hasBootstrapCreds || appChanged
659
- });
660
- } catch (error) {
661
- nitro.logger.warn("Auto-bootstrap failed:", error instanceof Error ? error.message : String(error));
662
- }
663
- }
664
- /**
665
- * Subscribe the tenant, optionally (re)write bootstrap credentials, and persist
666
- * the synced state to the local cache.
667
- * @param nitro
668
- * @param opts
669
- */
670
- async function finalizeBootstrap(nitro, opts) {
671
- await subscribeToApplication(opts.baseUrl, opts.developmentTenant, opts.appId, opts.authHeader);
672
- if (opts.forceCredentials) {
673
- const credentials = await getBootstrapCredentials(opts.baseUrl, opts.appId, opts.authHeader);
674
- const envFileName = await writeBootstrapCredentials(nitro.options.rootDir, {
675
- C8Y_BOOTSTRAP_TENANT: credentials.tenant,
676
- C8Y_BOOTSTRAP_USER: credentials.name,
677
- C8Y_BOOTSTRAP_PASSWORD: credentials.password
678
- });
679
- process.env.C8Y_BOOTSTRAP_TENANT = credentials.tenant;
680
- process.env.C8Y_BOOTSTRAP_USER = credentials.name;
681
- process.env.C8Y_BOOTSTRAP_PASSWORD = credentials.password;
682
- nitro.logger.debug(`Bootstrap credentials written to ${envFileName}`);
683
- }
684
- await writeManifestCache(nitro.options.rootDir, {
685
- manifestHash: opts.manifestHash,
686
- target: opts.target,
687
- appId: opts.appId
688
- });
689
- }
690
- //#endregion
691
552
  //#region src/index.ts
692
553
  function c8y() {
693
554
  return {
@@ -747,7 +608,7 @@ function c8y() {
747
608
  }
748
609
  });
749
610
  nitro.hooks.hook("close", async () => {
750
- if (nitro.options.preset !== "nitro-dev") await createC8yZip(nitro, options.zip);
611
+ if (nitro.options.preset !== "nitro-dev") await createC8yZip(nitro, options.zip, options.docker);
751
612
  });
752
613
  }
753
614
  };
@@ -0,0 +1,57 @@
1
+ import { createError } from "evlog";
2
+ import "evlog/nitro/v3";
3
+ //#region src/utils/logging.ts
4
+ /**
5
+ * Create a structured error for a Cumulocity microservice. Always prefer this
6
+ * over Nitro/h3's built-in `createError`: the returned error is captured in the
7
+ * wide log event **and** serialized into the JSON response under a `data` key.
8
+ *
9
+ * ## What the caller sees vs what stays on the server
10
+ *
11
+ * Every field **except `internal`** is sent to the client in the HTTP response.
12
+ * `internal` is stripped from the response and only ever appears in your logs.
13
+ *
14
+ * | Field | Sent to client? | Purpose |
15
+ * | ---------- | :-------------: | ------------------------------------------------------------- |
16
+ * | `message` | yes | Short, safe summary of what went wrong. |
17
+ * | `status` | yes | HTTP status code (default `500`). |
18
+ * | `code` | yes | Stable machine-readable id (e.g. `'PAYMENT_DECLINED'`). |
19
+ * | `why` | yes | Human-readable cause — safe to expose, no secrets. |
20
+ * | `fix` | yes | What the caller can do about it. |
21
+ * | `link` | yes | Docs URL with more detail. |
22
+ * | `cause` | no (logged) | The original `Error` you caught. |
23
+ * | `internal` | no (logged) | Backend-only diagnostics — never reaches the client. |
24
+ *
25
+ * ## Do NOT leak internal system errors
26
+ *
27
+ * Put anything sensitive or diagnostic (raw Cumulocity core responses, DB
28
+ * errors, tokens, tenant internals) in `internal`, **not** in `message`/`why`.
29
+ * `internal` is logged for debugging but never serialized to the response.
30
+ *
31
+ * @example
32
+ * // User-facing error — every field here is visible to the caller:
33
+ * throw createError({
34
+ * code: 'PAYMENT_DECLINED',
35
+ * message: 'Payment failed',
36
+ * status: 402,
37
+ * why: 'Card declined by issuer',
38
+ * fix: 'Try a different payment method',
39
+ * link: 'https://docs.example.com/payments/declined',
40
+ * })
41
+ *
42
+ * @example
43
+ * // Wrapping an upstream failure — keep the raw payload server-side only:
44
+ * catch (cause) {
45
+ * throw createError({
46
+ * status: 502,
47
+ * message: 'Upstream request failed', // safe, generic — what the caller sees
48
+ * cause: cause as Error,
49
+ * internal: { upstreamStatus: cause?.res?.status, upstreamBody: cause?.data },
50
+ * })
51
+ * }
52
+ *
53
+ * @see https://www.evlog.dev
54
+ */
55
+ const createError$1 = createError;
56
+ //#endregion
57
+ export { createError$1 as t };
@@ -0,0 +1,179 @@
1
+ import { c as getTenantOptionsByCategory, f as upsertTenantOption, i as deleteTenantOption, n as createBasicAuthHeader, p as createC8yManifest, s as getCurrentApplicationSettings } from "./c8y-api-DscUs-Kd.mjs";
2
+ import { n as readBootstrapCredentialsFromEnv, r as syncBootstrappedMicroservice } from "./autoBootstrap-Drk7ltXK.mjs";
3
+ import { n as validateBootstrapEnv, t as loadC8yConfig } from "./config-BVQHqlgV.mjs";
4
+ import process from "node:process";
5
+ import { defineCommand } from "citty";
6
+ import { consola } from "consola";
7
+ import { createInterface } from "node:readline/promises";
8
+ //#region src/cli/commands/options.ts
9
+ var options_default = defineCommand({
10
+ meta: {
11
+ name: "options",
12
+ description: "Manage tenant options on the development tenant"
13
+ },
14
+ args: {},
15
+ async run() {
16
+ consola.info("Loading configuration...");
17
+ const { env, nitroConfig, configDir } = await loadC8yConfig();
18
+ consola.info("Validating environment variables...");
19
+ const envVars = validateBootstrapEnv(env);
20
+ consola.info("Loading manifest...");
21
+ const manifest = await createC8yManifest(configDir, nitroConfig);
22
+ const category = manifest.settingsCategory || manifest.contextPath || manifest.name;
23
+ if (!manifest.settings || manifest.settings.length === 0) throw new Error("No settings defined in manifest. Add settings to your c8y.manifest configuration.");
24
+ consola.success(`Using category: ${category}`);
25
+ const devAuthHeader = createBasicAuthHeader(envVars.C8Y_DEVELOPMENT_TENANT, envVars.C8Y_DEVELOPMENT_USER, envVars.C8Y_DEVELOPMENT_PASSWORD);
26
+ let bootstrapCredentials;
27
+ try {
28
+ consola.info("Checking microservice on development tenant...");
29
+ bootstrapCredentials = await syncBootstrappedMicroservice({
30
+ rootDir: configDir,
31
+ baseUrl: envVars.C8Y_BASEURL,
32
+ developmentTenant: envVars.C8Y_DEVELOPMENT_TENANT,
33
+ authHeader: devAuthHeader,
34
+ manifest,
35
+ logger: consola,
36
+ env
37
+ });
38
+ } catch (error) {
39
+ consola.warn(`Could not sync microservice with the development tenant: ${error instanceof Error ? error.message : String(error)}`);
40
+ bootstrapCredentials = readBootstrapCredentialsFromEnv(env);
41
+ }
42
+ const bootstrapAuthHeader = bootstrapCredentials ? createBasicAuthHeader(bootstrapCredentials.tenant, bootstrapCredentials.name, bootstrapCredentials.password) : void 0;
43
+ if (!bootstrapAuthHeader) consola.warn("No bootstrap credentials available - reading with the development user. Encrypted credentials.* options cannot be decrypted.");
44
+ consola.info("Fetching current tenant options...");
45
+ const currentOptions = await getTenantOptionsByCategory(envVars.C8Y_BASEURL, category, devAuthHeader);
46
+ if (bootstrapAuthHeader) try {
47
+ const decryptedSettings = await getCurrentApplicationSettings(envVars.C8Y_BASEURL, bootstrapAuthHeader);
48
+ for (const [key, value] of Object.entries(decryptedSettings)) if (currentOptions[key] !== void 0 || key.startsWith("credentials.")) currentOptions[key] = value;
49
+ } catch (error) {
50
+ consola.warn(`Could not read decrypted settings with the bootstrap user: ${error instanceof Error ? error.message : String(error)}`);
51
+ }
52
+ const availableKeys = manifest.settings.map((s) => s.key);
53
+ const setManifestKeys = availableKeys.filter((k) => currentOptions[k] !== void 0);
54
+ consola.success(`Found ${Object.keys(currentOptions).length} options set on tenant (${setManifestKeys.length} of ${availableKeys.length} manifest settings)`);
55
+ const ctx = {
56
+ baseUrl: envVars.C8Y_BASEURL,
57
+ category,
58
+ devAuthHeader,
59
+ bootstrapAuthHeader,
60
+ availableKeys,
61
+ currentOptions
62
+ };
63
+ const action = await consola.prompt("What do you want to do?", {
64
+ type: "select",
65
+ options: [
66
+ {
67
+ label: "Read option value",
68
+ value: "read"
69
+ },
70
+ {
71
+ label: "Update/Create option",
72
+ value: "update"
73
+ },
74
+ {
75
+ label: "Delete option(s)",
76
+ value: "delete"
77
+ }
78
+ ]
79
+ });
80
+ if (action === "read") await handleRead(ctx);
81
+ else if (action === "update") await handleUpdate(ctx);
82
+ else if (action === "delete") await handleDelete(ctx);
83
+ }
84
+ });
85
+ /**
86
+ * Handle reading options: prints all options at once (decrypted where
87
+ * possible), then - after the user confirms with Enter - rewrites the output
88
+ * in place with `credentials.*` values masked, so secrets don't linger in the
89
+ * terminal scrollback.
90
+ * @param ctx - The shared options context
91
+ */
92
+ async function handleRead(ctx) {
93
+ const { bootstrapAuthHeader, availableKeys, currentOptions } = ctx;
94
+ if (!bootstrapAuthHeader && availableKeys.some((k) => k.startsWith("credentials."))) consola.warn("Encrypted credentials.* options cannot be read in decrypted form without bootstrap credentials. Run `npx c8y-nitro bootstrap` first.");
95
+ const keys = [.../* @__PURE__ */ new Set([...availableKeys, ...Object.keys(currentOptions)])];
96
+ const padWidth = Math.max(...keys.map((k) => k.length));
97
+ const renderLine = (key, maskCredentials) => {
98
+ const value = currentOptions[key];
99
+ const shownValue = value === void 0 ? "(not set)" : maskCredentials && key.startsWith("credentials.") ? "****" : value;
100
+ const suffix = availableKeys.includes(key) ? "" : " (not in manifest)";
101
+ return ` ${key.padEnd(padWidth)} = ${shownValue}${suffix}`;
102
+ };
103
+ const revealedLines = keys.map((key) => renderLine(key, false));
104
+ process.stdout.write(`${revealedLines.join("\n")}\n`);
105
+ const rl = createInterface({
106
+ input: process.stdin,
107
+ output: process.stdout
108
+ });
109
+ await rl.question("Press <Enter> to hide credential values...");
110
+ rl.close();
111
+ if (process.stdout.isTTY) {
112
+ const columns = process.stdout.columns || 80;
113
+ const rowsFor = (line) => Math.max(1, Math.ceil(line.length / columns));
114
+ const rowsToErase = revealedLines.reduce((rows, line) => rows + rowsFor(line), 1);
115
+ process.stdout.write(`\x1B[${rowsToErase}A\x1B[0J`);
116
+ process.stdout.write(`${keys.map((key) => renderLine(key, true)).join("\n")}\n`);
117
+ }
118
+ }
119
+ /**
120
+ * Handle updating options (with loop for multiple updates)
121
+ * @param ctx - The shared options context
122
+ */
123
+ async function handleUpdate(ctx) {
124
+ const { baseUrl, category, devAuthHeader, bootstrapAuthHeader, availableKeys, currentOptions } = ctx;
125
+ let continueUpdating = true;
126
+ while (continueUpdating) {
127
+ const key = await consola.prompt("Select option to update:", {
128
+ type: "select",
129
+ options: availableKeys.map((k) => ({
130
+ label: currentOptions[k] !== void 0 ? `${k} (current: ${currentOptions[k]})` : k.startsWith("credentials.") && !bootstrapAuthHeader ? `${k} (current: unknown)` : `${k} (not set)`,
131
+ value: k
132
+ })),
133
+ cancel: "reject"
134
+ });
135
+ const currentValue = currentOptions[key];
136
+ const newValue = await consola.prompt("Enter new value:", {
137
+ type: "text",
138
+ default: currentValue,
139
+ cancel: "reject"
140
+ });
141
+ consola.info(`Updating option: ${key}`);
142
+ await upsertTenantOption(baseUrl, category, key, newValue, devAuthHeader);
143
+ currentOptions[key] = newValue;
144
+ consola.success(`Option '${key}' updated successfully`);
145
+ if (!await consola.prompt("Update another option?", {
146
+ type: "confirm",
147
+ initial: false,
148
+ cancel: "reject"
149
+ })) continueUpdating = false;
150
+ }
151
+ }
152
+ /**
153
+ * Handle deleting multiple options
154
+ * @param ctx - The shared options context
155
+ */
156
+ async function handleDelete(ctx) {
157
+ const { baseUrl, category, devAuthHeader, bootstrapAuthHeader, availableKeys, currentOptions } = ctx;
158
+ const allKeys = bootstrapAuthHeader ? availableKeys.filter((k) => currentOptions[k] !== void 0) : [...availableKeys.filter((k) => !k.startsWith("credentials.") && currentOptions[k] !== void 0), ...availableKeys.filter((k) => k.startsWith("credentials."))];
159
+ if (allKeys.length === 0) {
160
+ consola.warn("No options are currently set");
161
+ return;
162
+ }
163
+ const keysToDelete = await consola.prompt("Select option(s) to delete:", {
164
+ type: "multiselect",
165
+ options: allKeys,
166
+ required: true,
167
+ cancel: "reject"
168
+ });
169
+ consola.info(`Deleting ${keysToDelete.length} option(s)...`);
170
+ for (const key of keysToDelete) {
171
+ consola.info(`Deleting option: ${key}`);
172
+ await deleteTenantOption(baseUrl, category, key, devAuthHeader);
173
+ delete currentOptions[key];
174
+ consola.success(`✓ Deleted: ${key}`);
175
+ }
176
+ consola.success("Delete operation completed");
177
+ }
178
+ //#endregion
179
+ export { options_default as default };
@@ -1,6 +1,6 @@
1
1
  //#region package.json
2
2
  var name = "c8y-nitro";
3
- var version = "0.7.0";
3
+ var version = "0.7.2";
4
4
  var description = "Lightning fast Cumulocity IoT microservice development powered by Nitro";
5
5
  //#endregion
6
6
  export { name as n, version as r, description as t };
@@ -1,4 +1,4 @@
1
- import { n as createBasicAuthHeader, p as createC8yManifest, t as assignUserRole, u as unassignUserRole } from "./c8y-api-BFjK6AYb.mjs";
1
+ import { n as createBasicAuthHeader, p as createC8yManifest, t as assignUserRole, u as unassignUserRole } from "./c8y-api-DscUs-Kd.mjs";
2
2
  import { n as validateBootstrapEnv, t as loadC8yConfig } from "./config-BVQHqlgV.mjs";
3
3
  import { defineCommand } from "citty";
4
4
  import { consola } from "consola";