create-colonel 1.1.1 → 1.1.3

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 CHANGED
@@ -26,6 +26,33 @@ Optional flag:
26
26
  bunx create-colonel my-app --skip-install
27
27
  ```
28
28
 
29
+ Telemetry flags:
30
+
31
+ ```bash
32
+ bunx create-colonel my-app \
33
+ --telemetry yes \
34
+ --telemetry-endpoint https://colonel-telemetry.vercel.app/api/ingest
35
+ ```
36
+
37
+ By default, when telemetry consent is `yes`, create-colonel uses the public provisioning endpoint to fetch app credentials automatically.
38
+
39
+ Optional secure provisioning override:
40
+
41
+ ```bash
42
+ bunx create-colonel my-app \
43
+ --telemetry yes \
44
+ --telemetry-endpoint https://colonel-telemetry.vercel.app/api/ingest \
45
+ --telemetry-provision-endpoint https://colonel-telemetry.vercel.app/api/provision-app \
46
+ --telemetry-provision-token <token>
47
+ ```
48
+
49
+ When telemetry is enabled and provisioning succeeds, the scaffolded `.env` receives:
50
+
51
+ - `COLONEL_TELEMETRY_ENABLED`
52
+ - `COLONEL_TELEMETRY_ENDPOINT`
53
+ - `COLONEL_TELEMETRY_APP_ID`
54
+ - `COLONEL_TELEMETRY_KEY`
55
+
29
56
  Use `--skip-install` when you only want scaffolded files and prefer to install dependencies later.
30
57
 
31
58
  Then run your new app:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-colonel",
3
- "version": "1.1.1",
3
+ "version": "1.1.3",
4
4
  "type": "module",
5
5
  "files": [
6
6
  "src",
package/src/cli.ts CHANGED
@@ -2,21 +2,167 @@
2
2
 
3
3
  import { cpSync, existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from "node:fs";
4
4
  import { basename, resolve } from "node:path";
5
+ import { createInterface } from "node:readline/promises";
6
+ import { stdin as input, stdout as output } from "node:process";
7
+
8
+ type TelemetryConsent = "yes" | "no";
9
+
10
+ const DEFAULT_TELEMETRY_ENDPOINT = "https://colonel-telemetry.vercel.app/api/ingest";
11
+
12
+ const deriveProvisionEndpoint = (ingestEndpoint: string, publicProvision = false): string => {
13
+ const suffix = publicProvision ? "/api/provision-public" : "/api/provision-app";
14
+
15
+ if (ingestEndpoint.endsWith("/api/ingest")) {
16
+ return `${ingestEndpoint.slice(0, -"/api/ingest".length)}${suffix}`;
17
+ }
18
+
19
+ return `${ingestEndpoint.replace(/\/$/, "")}${suffix}`;
20
+ };
21
+
22
+ const upsertEnv = (content: string, key: string, value: string): string => {
23
+ const line = `${key}=${value}`;
24
+ const pattern = new RegExp(`^${key}=.*$`, "m");
25
+ if (pattern.test(content)) {
26
+ return content.replace(pattern, line);
27
+ }
28
+
29
+ const suffix = content.length === 0 || content.endsWith("\n") ? "" : "\n";
30
+ return `${content}${suffix}${line}\n`;
31
+ };
32
+
33
+ const normalizeConsent = (value: string | undefined): TelemetryConsent | null => {
34
+ if (!value) return null;
35
+
36
+ const normalized = value.trim().toLowerCase();
37
+ if (["y", "yes", "true", "1"].includes(normalized)) return "yes";
38
+ if (["n", "no", "false", "0"].includes(normalized)) return "no";
39
+ return null;
40
+ };
41
+
42
+ const parseOptionValue = (args: string[], name: string): string | undefined => {
43
+ const index = args.indexOf(name);
44
+ if (index === -1) return undefined;
45
+ return args[index + 1];
46
+ };
47
+
48
+ const trackScaffoldEvent = async (payload: Record<string, unknown>, endpoint: string, appId: string, apiKey: string): Promise<void> => {
49
+ try {
50
+ await fetch(endpoint, {
51
+ method: "POST",
52
+ headers: {
53
+ "content-type": "application/json",
54
+ "x-colonel-telemetry-key": apiKey,
55
+ },
56
+ body: JSON.stringify({
57
+ name: "scaffold_created",
58
+ timestamp: new Date().toISOString(),
59
+ source: "create-colonel",
60
+ appId,
61
+ environment: process.env.NODE_ENV ?? "development",
62
+ runtime: "bun",
63
+ properties: payload,
64
+ }),
65
+ });
66
+ } catch {
67
+ // Do not block scaffolding on telemetry transport failures.
68
+ }
69
+ };
70
+
71
+ const provisionTelemetryApp = async (
72
+ appName: string,
73
+ source: string,
74
+ provisionEndpoint: string,
75
+ provisionToken?: string
76
+ ): Promise<{ appId: string; ingestKey: string } | null> => {
77
+ try {
78
+ const headers: Record<string, string> = {
79
+ "content-type": "application/json",
80
+ };
81
+
82
+ if (provisionToken) {
83
+ headers.authorization = `Bearer ${provisionToken}`;
84
+ }
85
+
86
+ const response = await fetch(provisionEndpoint, {
87
+ method: "POST",
88
+ headers,
89
+ body: JSON.stringify({ appName, source }),
90
+ });
91
+
92
+ if (!response.ok) return null;
93
+ const data = (await response.json()) as { appId?: string; ingestKey?: string };
94
+ if (!data.appId || !data.ingestKey) return null;
95
+
96
+ return { appId: data.appId, ingestKey: data.ingestKey };
97
+ } catch {
98
+ return null;
99
+ }
100
+ };
101
+
102
+ const askTelemetryConsent = async (): Promise<TelemetryConsent> => {
103
+ if (!input.isTTY || !output.isTTY) {
104
+ return "no";
105
+ }
106
+
107
+ const rl = createInterface({ input, output });
108
+ const answer = await rl.question("Share anonymous usage stats to help improve Colonel? (y/N): ");
109
+ rl.close();
110
+
111
+ return normalizeConsent(answer) ?? "no";
112
+ };
113
+
114
+ const configureTelemetryEnv = (
115
+ targetDir: string,
116
+ consent: TelemetryConsent,
117
+ endpoint: string,
118
+ appId?: string,
119
+ apiKey?: string
120
+ ): void => {
121
+ const envPath = resolve(targetDir, ".env");
122
+ const existing = existsSync(envPath) ? readFileSync(envPath, "utf8") : "";
123
+
124
+ let content = upsertEnv(existing, "COLONEL_TELEMETRY_ENABLED", consent === "yes" ? "true" : "false");
125
+ content = upsertEnv(content, "COLONEL_TELEMETRY_ENDPOINT", endpoint);
126
+ content = upsertEnv(content, "COLONEL_TELEMETRY_APP_ID", appId ?? "");
127
+ content = upsertEnv(content, "COLONEL_TELEMETRY_KEY", apiKey ?? "");
128
+ writeFileSync(envPath, content);
129
+ };
5
130
 
6
131
  const args = process.argv.slice(2);
7
132
  const targetArg = args.find((arg) => !arg.startsWith("-"));
8
133
  const skipInstall = args.includes("--skip-install");
9
134
  const showHelp = args.includes("--help") || args.includes("-h");
135
+ const telemetryFlag = parseOptionValue(args, "--telemetry");
136
+ const telemetryConsentFromFlag = normalizeConsent(telemetryFlag);
137
+ const telemetryEndpoint = parseOptionValue(args, "--telemetry-endpoint") ?? process.env.COLONEL_TELEMETRY_ENDPOINT ?? DEFAULT_TELEMETRY_ENDPOINT;
138
+ const secureProvisionEndpoint = parseOptionValue(args, "--telemetry-provision-endpoint")
139
+ ?? process.env.COLONEL_TELEMETRY_PROVISION_ENDPOINT
140
+ ?? deriveProvisionEndpoint(telemetryEndpoint, false);
141
+ const publicProvisionEndpoint = parseOptionValue(args, "--telemetry-public-provision-endpoint")
142
+ ?? process.env.COLONEL_TELEMETRY_PUBLIC_PROVISION_ENDPOINT
143
+ ?? deriveProvisionEndpoint(telemetryEndpoint, true);
144
+ const provisionToken = parseOptionValue(args, "--telemetry-provision-token")
145
+ ?? process.env.COLONEL_TELEMETRY_PROVISION_TOKEN;
10
146
 
11
147
  if (showHelp) {
12
- console.log("Usage: bunx create-colonel <project-name> [--skip-install]");
148
+ console.log("Usage: bunx create-colonel <project-name> [--skip-install] [--telemetry yes|no] [--telemetry-endpoint <url>] [--telemetry-provision-endpoint <url>] [--telemetry-public-provision-endpoint <url>] [--telemetry-provision-token <token>]");
13
149
  console.log("\nOptions:");
14
150
  console.log(" --skip-install Scaffold files without running bun install");
151
+ console.log(" --telemetry Set anonymous telemetry consent without interactive prompt");
152
+ console.log(" --telemetry-endpoint Override telemetry ingestion endpoint");
153
+ console.log(" --telemetry-provision-endpoint Override telemetry app provisioning endpoint");
154
+ console.log(" --telemetry-public-provision-endpoint Override public app provisioning endpoint");
155
+ console.log(" --telemetry-provision-token Bearer token used for telemetry app provisioning");
15
156
  process.exit(0);
16
157
  }
17
158
 
18
159
  if (!targetArg) {
19
- console.error("Usage: bunx create-colonel <project-name> [--skip-install]");
160
+ console.error("Usage: bunx create-colonel <project-name> [--skip-install] [--telemetry yes|no] [--telemetry-endpoint <url>] [--telemetry-provision-endpoint <url>] [--telemetry-public-provision-endpoint <url>] [--telemetry-provision-token <token>]");
161
+ process.exit(1);
162
+ }
163
+
164
+ if (telemetryFlag && !telemetryConsentFromFlag) {
165
+ console.error("Invalid --telemetry value. Use yes or no.");
20
166
  process.exit(1);
21
167
  }
22
168
 
@@ -46,6 +192,34 @@ if (existsSync(resolve(localFrameworkPath, "package.json"))) {
46
192
 
47
193
  writeFileSync(packageJsonPath, `${JSON.stringify(packageJson, null, 2)}\n`);
48
194
 
195
+ const consent = telemetryConsentFromFlag ?? await askTelemetryConsent();
196
+ let provisionedAppId: string | undefined;
197
+ let provisionedIngestKey: string | undefined;
198
+
199
+ if (consent === "yes") {
200
+ const provisioned = provisionToken
201
+ ? await provisionTelemetryApp(
202
+ basename(targetDir),
203
+ "create-colonel",
204
+ secureProvisionEndpoint,
205
+ provisionToken
206
+ )
207
+ : await provisionTelemetryApp(
208
+ basename(targetDir),
209
+ "create-colonel",
210
+ publicProvisionEndpoint
211
+ );
212
+
213
+ provisionedAppId = provisioned?.appId;
214
+ provisionedIngestKey = provisioned?.ingestKey;
215
+
216
+ if (!provisionedAppId || !provisionedIngestKey) {
217
+ console.warn("Telemetry consented but provisioning did not return credentials.");
218
+ }
219
+ }
220
+
221
+ configureTelemetryEnv(targetDir, consent, telemetryEndpoint, provisionedAppId, provisionedIngestKey);
222
+
49
223
  if (!skipInstall) {
50
224
  console.log("Installing dependencies...");
51
225
  const install = Bun.spawnSync(["bun", "install"], {
@@ -60,6 +234,14 @@ if (!skipInstall) {
60
234
  }
61
235
  }
62
236
 
237
+ if (consent === "yes" && provisionedAppId && provisionedIngestKey) {
238
+ await trackScaffoldEvent({
239
+ projectName: basename(targetDir),
240
+ skipInstall,
241
+ template: "create-colonel",
242
+ }, telemetryEndpoint, provisionedAppId, provisionedIngestKey);
243
+ }
244
+
63
245
  console.log("\nColonel app created successfully.\n");
64
246
  console.log(` cd ${targetArg}`);
65
247
 
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "colonel-app",
3
- "version": "1.0.1",
3
+ "version": "1.1.1",
4
4
  "type": "module",
5
5
  "private": true,
6
6
  "scripts": {
@@ -8,7 +8,7 @@
8
8
  "upgrade:colonel": "bun add @coloneldev/framework@latest"
9
9
  },
10
10
  "dependencies": {
11
- "@coloneldev/framework": "^1.0.2",
11
+ "@coloneldev/framework": "^1.1.2",
12
12
  "ejs": "^5.0.1"
13
13
  },
14
14
  "devDependencies": {
@@ -1,4 +1,4 @@
1
- import { Kernel } from "@coloneldev/framework";
1
+ import { Kernel, createTelemetryClientFromEnv } from "@coloneldev/framework";
2
2
  import webRouter from "../config/routes/web";
3
3
  import { existsSync } from "fs";
4
4
  import { extensions } from "../config/acceptedStaticContentTypes";
@@ -14,6 +14,10 @@ const viewsRoot = path.resolve(import.meta.dir, "..", "..", "resources", "views"
14
14
  const publicRoot = path.resolve(import.meta.dir, "..", "..", "public");
15
15
  const controllerRoot = path.resolve(import.meta.dir, "..", "app", "Http", "Controllers");
16
16
  const container = new Container();
17
+ const telemetry = createTelemetryClientFromEnv({
18
+ source: "colonel-app",
19
+ appId: process.env.COLONEL_TELEMETRY_APP_ID ?? process.env.appName ?? "Colonel",
20
+ });
17
21
 
18
22
  container.singleton(
19
23
  AppInfoService,
@@ -32,6 +36,10 @@ export const server = () => {
32
36
  session: {
33
37
  enabled: true,
34
38
  },
39
+ telemetry: {
40
+ client: telemetry,
41
+ sampleRate: 0.2,
42
+ },
35
43
  controllerResolver: async (name: string) => {
36
44
  const modulePath = `${controllerRoot}/${name}.ts`;
37
45
  const mod = await import(modulePath);
@@ -45,6 +53,11 @@ export const server = () => {
45
53
  }
46
54
  }, container);
47
55
  const PORT = Number(process.env.PORT) || 5000;
56
+ telemetry.track("server_start", {
57
+ port: PORT,
58
+ environment: process.env.NODE_ENV ?? "development",
59
+ runtime: "bun",
60
+ });
48
61
 
49
62
  console.log(`Server running at http://localhost:${PORT}/`);
50
63