c8y-nitro 0.6.0 → 0.7.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.
@@ -1,6 +1,6 @@
1
- import { a as findMicroserviceByName, d as updateMicroservice, l as subscribeToApplication, n as createBasicAuthHeader, o as getBootstrapCredentials, p as createC8yManifest, r as createMicroservice } from "./c8y-api-BbRS1-Ls.mjs";
2
- import { t as writeBootstrapCredentials } from "./env-file-B0BK-uZW.mjs";
3
- import { n as validateBootstrapEnv, t as loadC8yConfig } from "./config-BRnvtthI.mjs";
1
+ import { a as findMicroserviceByName, d as updateMicroservice, l as subscribeToApplication, 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 validateBootstrapEnv, t as loadC8yConfig } from "./config-BVQHqlgV.mjs";
4
4
  import { defineCommand, runCommand } from "citty";
5
5
  import { consola } from "consola";
6
6
  //#region src/cli/commands/bootstrap.ts
@@ -12,11 +12,11 @@ var bootstrap_default = defineCommand({
12
12
  args: {},
13
13
  async run() {
14
14
  consola.info("Loading configuration...");
15
- const { env, c8yOptions, configDir } = await loadC8yConfig();
15
+ const { env, nitroConfig, configDir } = await loadC8yConfig();
16
16
  consola.info("Validating environment variables...");
17
17
  const envVars = validateBootstrapEnv(env);
18
18
  consola.info("Building manifest...");
19
- const manifest = await createC8yManifest(configDir, c8yOptions?.manifest);
19
+ const manifest = await createC8yManifest(configDir, nitroConfig);
20
20
  consola.success(`Manifest created for: ${manifest.name} v${manifest.version}`);
21
21
  const authHeader = createBasicAuthHeader(envVars.C8Y_DEVELOPMENT_TENANT, envVars.C8Y_DEVELOPMENT_USER, envVars.C8Y_DEVELOPMENT_PASSWORD);
22
22
  consola.info(`Checking if microservice "${manifest.name}" exists...`);
@@ -52,7 +52,7 @@ var bootstrap_default = defineCommand({
52
52
  });
53
53
  consola.success(`Bootstrap credentials written to ${envFileName}`);
54
54
  if (manifest.roles && manifest.roles.length > 0) {
55
- if (await consola.prompt("Do you want to manage microservice roles for your development user?", { type: "confirm" })) await runCommand(await import("./roles-D7rpbxPp.mjs").then((r) => r.default), { rawArgs: [] });
55
+ if (await consola.prompt("Do you want to manage microservice roles for your development user?", { type: "confirm" })) await runCommand(await import("./roles-B58Fguop.mjs").then((r) => r.default), { rawArgs: [] });
56
56
  }
57
57
  consola.success("Bootstrap complete!");
58
58
  }
@@ -6,8 +6,29 @@ const GENERATED_READINESS_ROUTE = "/_c8y_nitro/readiness";
6
6
  const GENERATED_INVALIDATE_TENANT_OPTIONS_ROUTE = "/_c8y_nitro/invalidate-tenant-options";
7
7
  //#endregion
8
8
  //#region src/module/manifest.ts
9
+ const DEFAULT_OPENAPI_ROUTE = "/_openapi.json";
9
10
  const ROLE_OPTION_MANAGEMENT_READ = "ROLE_OPTION_MANAGEMENT_READ";
10
- const ROLE_OPTION_MANAGEMENT_ADMIN = "ROLE_OPTION_MANAGEMENT_ADMIN";
11
+ /**
12
+ * Decide whether Nitro will expose an OpenAPI spec in the deployed build,
13
+ * and if so return the configured route (with leading slash).
14
+ * Returns null when OpenAPI is not enabled for production.
15
+ * @param config - Nitro config (or options) to inspect
16
+ */
17
+ function resolveNitroOpenApiRoute(config) {
18
+ if (!config.experimental?.openAPI) return null;
19
+ if (!config.openAPI?.production) return null;
20
+ return config.openAPI?.route || DEFAULT_OPENAPI_ROUTE;
21
+ }
22
+ /**
23
+ * Pull the parts of a Nitro config that feed manifest generation.
24
+ * @param config - Nitro config (or options) loaded from disk or from a Nitro instance
25
+ */
26
+ function extractManifestInputs(config) {
27
+ return {
28
+ options: config.c8y?.manifest ?? {},
29
+ openApiSpec: resolveNitroOpenApiRoute(config) ?? void 0
30
+ };
31
+ }
11
32
  function validateManifestSettings(options) {
12
33
  const invalidSettings = options.settings?.filter((setting) => typeof setting.defaultValue !== "string" || setting.defaultValue.length === 0) ?? [];
13
34
  if (invalidSettings.length === 0) return;
@@ -39,13 +60,18 @@ async function readPackageJsonFieldsForManifest(rootDir, logger) {
39
60
  };
40
61
  }
41
62
  /**
42
- * Creates a Cumulocity manifest from rootDir and options.
43
- * Standalone function that can be used by CLI or module.
44
- * @param rootDir - Directory containing package.json
45
- * @param options - Manifest options
63
+ * Create a Cumulocity manifest from a project root and its Nitro config.
64
+ * Reads `package.json` for identity fields, takes user manifest options from
65
+ * `nitroConfig.c8y.manifest`, and auto-injects `openApiSpec` when Nitro
66
+ * OpenAPI is enabled for production.
67
+ *
68
+ * Used by both the Nitro module and the CLI commands.
69
+ * @param rootDir - Directory containing the project's `package.json`
70
+ * @param nitroConfig - Nitro config (or options) for this project
46
71
  * @param logger - Optional logger for debug output
47
72
  */
48
- async function createC8yManifest(rootDir, options = {}, logger) {
73
+ async function createC8yManifest(rootDir, nitroConfig = {}, logger) {
74
+ const { options, openApiSpec } = extractManifestInputs(nitroConfig);
49
75
  validateManifestSettings(options);
50
76
  const { name, version, provider, ...restManifestFields } = await readPackageJsonFieldsForManifest(rootDir, logger);
51
77
  const probeFields = {};
@@ -59,7 +85,7 @@ async function createC8yManifest(rootDir, options = {}, logger) {
59
85
  };
60
86
  const key = `${name}-key`;
61
87
  let requiredRoles = options.requiredRoles ? [...options.requiredRoles] : void 0;
62
- if (options.settings && options.settings.length > 0 && !requiredRoles?.includes(ROLE_OPTION_MANAGEMENT_READ) && !requiredRoles?.includes(ROLE_OPTION_MANAGEMENT_ADMIN)) {
88
+ if (options.settings && options.settings.length > 0 && !requiredRoles?.includes(ROLE_OPTION_MANAGEMENT_READ)) {
63
89
  requiredRoles = [...requiredRoles ?? [], ROLE_OPTION_MANAGEMENT_READ];
64
90
  logger?.debug(`Auto-added ${ROLE_OPTION_MANAGEMENT_READ} to requiredRoles because manifest.settings are defined`);
65
91
  }
@@ -68,6 +94,7 @@ async function createC8yManifest(rootDir, options = {}, logger) {
68
94
  ...probeFields,
69
95
  ...options,
70
96
  ...requiredRoles !== void 0 ? { requiredRoles } : {},
97
+ ...openApiSpec ? { openApiSpec } : {},
71
98
  provider,
72
99
  name,
73
100
  version,
@@ -75,6 +102,7 @@ async function createC8yManifest(rootDir, options = {}, logger) {
75
102
  key,
76
103
  type: "MICROSERVICE"
77
104
  };
105
+ if (openApiSpec) logger?.debug(`Auto-added openApiSpec="${openApiSpec}" to manifest (Nitro OpenAPI is enabled)`);
78
106
  logger?.debug(`Created Cumulocity manifest: ${JSON.stringify(manifest, null, 2)}`);
79
107
  return manifest;
80
108
  }
@@ -84,7 +112,7 @@ async function createC8yManifest(rootDir, options = {}, logger) {
84
112
  * @param nitro - The Nitro instance
85
113
  */
86
114
  async function createC8yManifestFromNitro(nitro) {
87
- return createC8yManifest(nitro.options.rootDir, nitro.options.c8y?.manifest, nitro.logger);
115
+ return createC8yManifest(nitro.options.rootDir, nitro.options, nitro.logger);
88
116
  }
89
117
  //#endregion
90
118
  //#region src/cli/utils/c8y-api.ts
@@ -1,4 +1,4 @@
1
- import { n as name, r as version, t as description } from "../package-DsLC9Mo3.mjs";
1
+ import { n as name, r as version, t as description } from "../package-BhFLRPOb.mjs";
2
2
  import { defineCommand, runMain } from "citty";
3
3
  //#region src/cli/index.ts
4
4
  runMain(defineCommand({
@@ -8,9 +8,9 @@ runMain(defineCommand({
8
8
  description
9
9
  },
10
10
  subCommands: {
11
- bootstrap: () => import("../bootstrap-DUDpmXcU.mjs").then((r) => r.default),
12
- roles: () => import("../roles-D7rpbxPp.mjs").then((r) => r.default),
13
- options: () => import("../options-Web5UyIU.mjs").then((r) => r.default)
11
+ bootstrap: () => import("../bootstrap-ObaEF7rN.mjs").then((r) => r.default),
12
+ roles: () => import("../roles-B58Fguop.mjs").then((r) => r.default),
13
+ options: () => import("../options-B7_yb_wr.mjs").then((r) => r.default)
14
14
  }
15
15
  }));
16
16
  //#endregion
@@ -1,5 +1,4 @@
1
1
  import { ICredentials } from "@c8y/client";
2
-
3
2
  //#region src/types/credentials.d.ts
4
3
  type TenantCredentials = Record<string, ICredentials>;
5
4
  //#endregion
@@ -1,5 +1,5 @@
1
+ import "./credentials-eKnJ7m9Y.mjs";
1
2
  import { C8YTenantOptionKeysCacheConfig } from "c8y-nitro/types";
2
-
3
3
  //#region src/types/apiClient.d.ts
4
4
  interface C8YAPIClientOptions {
5
5
  /**
@@ -16,7 +16,7 @@ interface C8YAPIClientOptions {
16
16
  }
17
17
  //#endregion
18
18
  //#region src/types/manifest.d.ts
19
- type C8YManifestOptions = Omit<C8YManifest, 'name' | 'version' | 'apiVersion' | 'key' | 'type' | 'provider'>;
19
+ type C8YManifestOptions = Omit<C8YManifest, 'name' | 'version' | 'apiVersion' | 'key' | 'type' | 'provider' | 'openApiSpec'>;
20
20
  interface C8YManifest {
21
21
  /**
22
22
  * API version (e.g., "v2", "2"). Version 2 required for enhanced container security.
@@ -126,6 +126,16 @@ interface C8YManifest {
126
126
  * Platform extensions (e.g., UI plugins).
127
127
  */
128
128
  extensions?: Extension[];
129
+ /**
130
+ * OpenAPI specification path exposed by this microservice. Auto-populated
131
+ * by c8y-nitro when Nitro's `experimental.openAPI` is enabled and
132
+ * `openAPI.production` is configured. Used by clients (e.g., mc8yp) to
133
+ * auto-discover the API.
134
+ *
135
+ * Path is relative to the microservice context path.
136
+ * @example "/_openapi.json"
137
+ */
138
+ openApiSpec?: string;
129
139
  }
130
140
  /**
131
141
  * Company/organization information.
@@ -383,10 +393,16 @@ interface C8YRoles$1 {}
383
393
  */
384
394
  type C8YTenantOptionKeysCacheConfig$1 = Partial<Record<C8YTenantOptionKey$1, number>>;
385
395
  /**
386
- * Type for tenant option keys.
396
+ * Union of tenant option keys declared in `manifest.settings`.
387
397
  * Overwritten by generated types from manifest settings (manifest.settings[].key).
388
398
  */
389
399
  type C8YTenantOptionKey$1 = string;
400
+ /**
401
+ * The microservice's own tenant option settings category, resolved from
402
+ * `manifest.settingsCategory` (falling back to `contextPath`, then `name`).
403
+ * Overwritten by generated types from the manifest.
404
+ */
405
+ type C8YSettingsCategory$1 = string;
390
406
  //#endregion
391
407
  //#region src/types/index.d.ts
392
408
  interface C8yDevOptions {
@@ -428,4 +444,4 @@ interface C8yNitroModuleOptions {
428
444
  skipBootstrap?: boolean;
429
445
  }
430
446
  //#endregion
431
- export { C8YRoles$1 as a, C8YManifest as c, C8YTenantOptionKeysCacheConfig$1 as i, C8YManifestOptions as l, C8yNitroModuleOptions as n, C8yCacheOptions as o, C8YTenantOptionKey$1 as r, C8YZipOptions as s, C8yDevOptions as t, C8YAPIClientOptions as u };
447
+ export { C8YTenantOptionKeysCacheConfig$1 as a, C8YZipOptions as c, C8YAPIClientOptions as d, C8YTenantOptionKey$1 as i, C8YManifest as l, C8yNitroModuleOptions as n, C8YRoles$1 as o, C8YSettingsCategory$1 as r, C8yCacheOptions as s, C8yDevOptions as t, C8YManifestOptions as u };
package/dist/index.d.mts CHANGED
@@ -1,7 +1,6 @@
1
- import { n as C8yNitroModuleOptions } from "./index-uOwpI6rD.mjs";
2
- import { t as TenantCredentials } from "./credentials-9FO7rTIR.mjs";
1
+ import { n as C8yNitroModuleOptions } from "./index-ByDSMEvU.mjs";
2
+ import { t as TenantCredentials } from "./credentials-eKnJ7m9Y.mjs";
3
3
  import { NitroModule } from "nitro/types";
4
-
5
4
  //#region src/index.d.ts
6
5
  declare module 'nitro/types' {
7
6
  interface NitroOptions {
package/dist/index.mjs CHANGED
@@ -1,8 +1,8 @@
1
- import { _ as GENERATED_READINESS_ROUTE, a as findMicroserviceByName, 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-BbRS1-Ls.mjs";
2
- import { t as writeBootstrapCredentials } from "./env-file-B0BK-uZW.mjs";
3
- import { n as name } from "./package-DsLC9Mo3.mjs";
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";
4
4
  import { basename, dirname, join, relative } from "node:path";
5
- import { mkdir, writeFile } from "node:fs/promises";
5
+ import { mkdir, readFile, 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,6 +11,7 @@ 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";
14
15
  import process from "node:process";
15
16
  import evlog from "evlog/nitro/v3";
16
17
  //#region src/module/apiClient.ts
@@ -113,6 +114,7 @@ type C8ySerialize<T> = T extends void | undefined | null ? undefined : T extends
113
114
  * @param className - The class name for the generated service (e.g., "PlaygroundAPIClient")
114
115
  */
115
116
  function generateAPIClient(routes, contextPath, className) {
117
+ const methods = routes.map((route) => generateMethod(route)).join("\n\n");
116
118
  return `/* eslint-disable eslint-comments/no-unlimited-disable */
117
119
  /* eslint-disable */
118
120
  /**
@@ -157,7 +159,7 @@ export class ${className} {
157
159
  return JSON.parse(text);
158
160
  }
159
161
 
160
- ${routes.map((route) => generateMethod(route)).join("\n\n")}
162
+ ${methods}
161
163
  }`;
162
164
  }
163
165
  /**
@@ -262,6 +264,8 @@ async function buildDockerImage(nitro, c8yDir) {
262
264
  try {
263
265
  const result = await x("docker", [
264
266
  "build",
267
+ "--platform",
268
+ "linux/amd64",
265
269
  "-t",
266
270
  imageName,
267
271
  "-f",
@@ -370,6 +374,7 @@ function setupRuntime(nitro, manifest) {
370
374
  nitro.logger.debug("Setting up C8Y nitro runtime");
371
375
  const roles = manifest.roles ?? [];
372
376
  const settingKeys = (manifest.settings ?? []).map((s) => s.key);
377
+ const settingsCategory = manifest.settingsCategory ?? manifest.contextPath ?? manifest.name;
373
378
  const completeTypesDir = join$1(nitro.options.rootDir, nitro.options.typescript.generatedTypesDir ?? "node_modules/.nitro/types");
374
379
  const typesFile = join$1(completeTypesDir, "c8y-nitro.d.ts");
375
380
  const typesContent = `// generated by c8y-nitro
@@ -379,6 +384,7 @@ ${roles.map((role) => ` '${role}': '${role}';`).join("\n")}
379
384
  }
380
385
  type C8YTenantOptionKey = ${settingKeys.length > 0 ? `${settingKeys.map((k) => `'${k}'`).join(" | ")}` : "never"}
381
386
  type C8YTenantOptionKeysCacheConfig = Partial<Record<C8YTenantOptionKey, number>>;
387
+ type C8YSettingsCategory = ${settingsCategory ? `'${settingsCategory}'` : "string"};
382
388
  }
383
389
 
384
390
  declare module 'c8y-nitro/runtime' {
@@ -413,7 +419,7 @@ async function setupRuntimeConfig(nitro, options) {
413
419
  nitro.options.runtimeConfig.c8yCredentialsCacheTTL = options.cache?.credentialsTTL ?? 600;
414
420
  nitro.options.runtimeConfig.c8yDefaultTenantOptionsTTL = options.cache?.defaultTenantOptionsTTL ?? 600;
415
421
  nitro.options.runtimeConfig.c8yTenantOptionsPerKeyTTL = options.cache?.tenantOptions ?? {};
416
- const manifest = await createC8yManifest(nitro.options.rootDir, options.manifest, nitro.logger);
422
+ const manifest = await createC8yManifest(nitro.options.rootDir, nitro.options, nitro.logger);
417
423
  nitro.options.runtimeConfig.c8ySettingsCategory = options.manifest?.settingsCategory ?? manifest.contextPath ?? manifest.name;
418
424
  }
419
425
  //#endregion
@@ -515,39 +521,156 @@ function checkProbeEndpoint(nitro, probe, probeType) {
515
521
  }
516
522
  }
517
523
  //#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
518
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
+ ];
519
595
  /**
520
- * Automatically bootstraps the microservice to the development tenant if needed.
521
- * Runs silently - only logs if bootstrap was performed or if errors occur.
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.
522
609
  * @param nitro - Nitro instance
523
610
  */
524
611
  async function autoBootstrap(nitro) {
525
612
  try {
526
- if ([
527
- "C8Y_BOOTSTRAP_TENANT",
528
- "C8Y_BOOTSTRAP_USER",
529
- "C8Y_BOOTSTRAP_PASSWORD"
530
- ].every((v) => process.env[v])) return;
531
- if ([
532
- "C8Y_BASEURL",
533
- "C8Y_DEVELOPMENT_TENANT",
534
- "C8Y_DEVELOPMENT_USER",
535
- "C8Y_DEVELOPMENT_PASSWORD"
536
- ].filter((v) => !process.env[v]).length > 0) return;
613
+ const rootDir = nitro.options.rootDir;
614
+ if (DEV_ENV_VARS.filter((v) => !process.env[v]).length > 0) return;
537
615
  const baseUrl = process.env.C8Y_BASEURL.endsWith("/") ? process.env.C8Y_BASEURL.slice(0, -1) : process.env.C8Y_BASEURL;
538
- const authHeader = createBasicAuthHeader(process.env.C8Y_DEVELOPMENT_TENANT, process.env.C8Y_DEVELOPMENT_USER, process.env.C8Y_DEVELOPMENT_PASSWORD);
539
- const manifest = await createC8yManifest(nitro.options.rootDir, nitro.options.c8y?.manifest, nitro.logger);
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);
540
625
  const existingApp = await findMicroserviceByName(baseUrl, manifest.name, authHeader);
541
- let appId;
542
- if (existingApp) {
543
- appId = existingApp.id;
544
- nitro.logger.debug(`Microservice "${manifest.name}" already exists (ID: ${appId}), retrieving bootstrap credentials...`);
545
- } else {
546
- appId = (await createMicroservice(baseUrl, manifest, authHeader)).id;
547
- nitro.logger.debug(`Microservice "${manifest.name}" created (ID: ${appId})`);
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.`);
548
648
  }
549
- await subscribeToApplication(baseUrl, process.env.C8Y_DEVELOPMENT_TENANT, appId, authHeader);
550
- const credentials = await getBootstrapCredentials(baseUrl, appId, authHeader);
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);
551
674
  const envFileName = await writeBootstrapCredentials(nitro.options.rootDir, {
552
675
  C8Y_BOOTSTRAP_TENANT: credentials.tenant,
553
676
  C8Y_BOOTSTRAP_USER: credentials.name,
@@ -556,10 +679,13 @@ async function autoBootstrap(nitro) {
556
679
  process.env.C8Y_BOOTSTRAP_TENANT = credentials.tenant;
557
680
  process.env.C8Y_BOOTSTRAP_USER = credentials.name;
558
681
  process.env.C8Y_BOOTSTRAP_PASSWORD = credentials.password;
559
- nitro.logger.success(`Auto-bootstrap complete! Bootstrap credentials written to ${envFileName}`);
560
- } catch (error) {
561
- nitro.logger.warn("Auto-bootstrap failed:", error instanceof Error ? error.message : String(error));
682
+ nitro.logger.debug(`Bootstrap credentials written to ${envFileName}`);
562
683
  }
684
+ await writeManifestCache(nitro.options.rootDir, {
685
+ manifestHash: opts.manifestHash,
686
+ target: opts.target,
687
+ appId: opts.appId
688
+ });
563
689
  }
564
690
  //#endregion
565
691
  //#region src/index.ts
@@ -575,7 +701,8 @@ function c8y() {
575
701
  nitro.options.noExternals = nitro.options.noExternals === true ? true : [
576
702
  ...Array.isArray(nitro.options.noExternals) ? nitro.options.noExternals : [],
577
703
  name,
578
- "@c8y/client"
704
+ "@c8y/client",
705
+ "tslib"
579
706
  ];
580
707
  nitro.options.rolldownConfig = {
581
708
  ...nitro.options.rolldownConfig,
@@ -597,12 +724,13 @@ function c8y() {
597
724
  exclude: [manifest.livenessProbe?.httpGet?.path, manifest.readinessProbe?.httpGet?.path].filter(Boolean)
598
725
  });
599
726
  await setupEvlog(nitro);
600
- if (!options.skipBootstrap) await autoBootstrap(nitro);
727
+ if (nitro.options.preset === "nitro-dev" && !options.skipBootstrap) await autoBootstrap(nitro);
601
728
  await setupRuntimeConfig(nitro, options);
602
729
  setupRuntime(nitro, manifest);
603
730
  nitro.hooks.hook("dev:reload", async () => {
604
731
  manifest = await createC8yManifestFromNitro(nitro);
605
732
  setupRuntime(nitro, manifest);
733
+ if (!options.skipBootstrap) await autoBootstrap(nitro);
606
734
  if (options.apiClient) {
607
735
  nitro.logger.debug("Generating C8Y API client");
608
736
  await writeAPIClient(nitro, options);
@@ -1,5 +1,5 @@
1
- import { c as getTenantOptionsByCategory, f as upsertTenantOption, i as deleteTenantOption, n as createBasicAuthHeader, p as createC8yManifest, s as getTenantOption } from "./c8y-api-BbRS1-Ls.mjs";
2
- import { n as validateBootstrapEnv, t as loadC8yConfig } from "./config-BRnvtthI.mjs";
1
+ import { c as getTenantOptionsByCategory, f as upsertTenantOption, i as deleteTenantOption, n as createBasicAuthHeader, p as createC8yManifest, s as getTenantOption } from "./c8y-api-BFjK6AYb.mjs";
2
+ import { n as validateBootstrapEnv, t as loadC8yConfig } from "./config-BVQHqlgV.mjs";
3
3
  import { defineCommand } from "citty";
4
4
  import { consola } from "consola";
5
5
  //#region src/cli/commands/options.ts
@@ -11,11 +11,11 @@ var options_default = defineCommand({
11
11
  args: {},
12
12
  async run() {
13
13
  consola.info("Loading configuration...");
14
- const { env, c8yOptions, configDir } = await loadC8yConfig();
14
+ const { env, nitroConfig, configDir } = await loadC8yConfig();
15
15
  consola.info("Validating environment variables...");
16
16
  const envVars = validateBootstrapEnv(env);
17
17
  consola.info("Loading manifest...");
18
- const manifest = await createC8yManifest(configDir, c8yOptions?.manifest);
18
+ const manifest = await createC8yManifest(configDir, nitroConfig);
19
19
  const category = manifest.settingsCategory || manifest.contextPath || manifest.name;
20
20
  if (!manifest.settings || manifest.settings.length === 0) throw new Error("No settings defined in manifest. Add settings to your c8y.manifest configuration.");
21
21
  consola.success(`Using category: ${category}`);
@@ -1,6 +1,6 @@
1
1
  //#region package.json
2
2
  var name = "c8y-nitro";
3
- var version = "0.6.0";
3
+ var version = "0.7.0";
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,5 +1,5 @@
1
- import { n as createBasicAuthHeader, p as createC8yManifest, t as assignUserRole, u as unassignUserRole } from "./c8y-api-BbRS1-Ls.mjs";
2
- import { n as validateBootstrapEnv, t as loadC8yConfig } from "./config-BRnvtthI.mjs";
1
+ import { n as createBasicAuthHeader, p as createC8yManifest, t as assignUserRole, u as unassignUserRole } from "./c8y-api-BFjK6AYb.mjs";
2
+ import { n as validateBootstrapEnv, t as loadC8yConfig } from "./config-BVQHqlgV.mjs";
3
3
  import { defineCommand } from "citty";
4
4
  import { consola } from "consola";
5
5
  //#region src/cli/commands/roles.ts
@@ -11,11 +11,11 @@ var roles_default = defineCommand({
11
11
  args: {},
12
12
  async run() {
13
13
  consola.info("Loading configuration...");
14
- const { env, c8yOptions, configDir } = await loadC8yConfig();
14
+ const { env, nitroConfig, configDir } = await loadC8yConfig();
15
15
  consola.info("Validating environment variables...");
16
16
  const envVars = validateBootstrapEnv(env);
17
17
  consola.info("Building manifest...");
18
- const manifest = await createC8yManifest(configDir, c8yOptions?.manifest);
18
+ const manifest = await createC8yManifest(configDir, nitroConfig);
19
19
  const authHeader = createBasicAuthHeader(envVars.C8Y_DEVELOPMENT_TENANT, envVars.C8Y_DEVELOPMENT_USER, envVars.C8Y_DEVELOPMENT_PASSWORD);
20
20
  if (manifest.roles && manifest.roles.length > 0) {
21
21
  const rolesToAssign = await consola.prompt("Select roles to assign to your user (unselected roles will be removed):", {