create-colonel 1.1.1 → 1.1.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/README.md CHANGED
@@ -26,6 +26,23 @@ 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
+ --telemetry-provision-endpoint https://colonel-telemetry.vercel.app/api/provision-app \
36
+ --telemetry-provision-token <token>
37
+ ```
38
+
39
+ When telemetry is enabled and provisioning succeeds, the scaffolded `.env` receives:
40
+
41
+ - `COLONEL_TELEMETRY_ENABLED`
42
+ - `COLONEL_TELEMETRY_ENDPOINT`
43
+ - `COLONEL_TELEMETRY_APP_ID`
44
+ - `COLONEL_TELEMETRY_KEY`
45
+
29
46
  Use `--skip-install` when you only want scaffolded files and prefer to install dependencies later.
30
47
 
31
48
  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.2",
4
4
  "type": "module",
5
5
  "files": [
6
6
  "src",
package/src/cli.ts CHANGED
@@ -2,21 +2,156 @@
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): string => {
13
+ if (ingestEndpoint.endsWith("/api/ingest")) {
14
+ return `${ingestEndpoint.slice(0, -"/api/ingest".length)}/api/provision-app`;
15
+ }
16
+
17
+ return `${ingestEndpoint.replace(/\/$/, "")}/api/provision-app`;
18
+ };
19
+
20
+ const upsertEnv = (content: string, key: string, value: string): string => {
21
+ const line = `${key}=${value}`;
22
+ const pattern = new RegExp(`^${key}=.*$`, "m");
23
+ if (pattern.test(content)) {
24
+ return content.replace(pattern, line);
25
+ }
26
+
27
+ const suffix = content.length === 0 || content.endsWith("\n") ? "" : "\n";
28
+ return `${content}${suffix}${line}\n`;
29
+ };
30
+
31
+ const normalizeConsent = (value: string | undefined): TelemetryConsent | null => {
32
+ if (!value) return null;
33
+
34
+ const normalized = value.trim().toLowerCase();
35
+ if (["y", "yes", "true", "1"].includes(normalized)) return "yes";
36
+ if (["n", "no", "false", "0"].includes(normalized)) return "no";
37
+ return null;
38
+ };
39
+
40
+ const parseOptionValue = (args: string[], name: string): string | undefined => {
41
+ const index = args.indexOf(name);
42
+ if (index === -1) return undefined;
43
+ return args[index + 1];
44
+ };
45
+
46
+ const trackScaffoldEvent = async (payload: Record<string, unknown>, endpoint: string, appId: string, apiKey: string): Promise<void> => {
47
+ try {
48
+ await fetch(endpoint, {
49
+ method: "POST",
50
+ headers: {
51
+ "content-type": "application/json",
52
+ "x-colonel-telemetry-key": apiKey,
53
+ },
54
+ body: JSON.stringify({
55
+ name: "scaffold_created",
56
+ timestamp: new Date().toISOString(),
57
+ source: "create-colonel",
58
+ appId,
59
+ environment: process.env.NODE_ENV ?? "development",
60
+ runtime: "bun",
61
+ properties: payload,
62
+ }),
63
+ });
64
+ } catch {
65
+ // Do not block scaffolding on telemetry transport failures.
66
+ }
67
+ };
68
+
69
+ const provisionTelemetryApp = async (
70
+ appName: string,
71
+ source: string,
72
+ provisionEndpoint: string,
73
+ provisionToken: string
74
+ ): Promise<{ appId: string; ingestKey: string } | null> => {
75
+ try {
76
+ const response = await fetch(provisionEndpoint, {
77
+ method: "POST",
78
+ headers: {
79
+ "content-type": "application/json",
80
+ authorization: `Bearer ${provisionToken}`,
81
+ },
82
+ body: JSON.stringify({ appName, source }),
83
+ });
84
+
85
+ if (!response.ok) return null;
86
+ const data = (await response.json()) as { appId?: string; ingestKey?: string };
87
+ if (!data.appId || !data.ingestKey) return null;
88
+
89
+ return { appId: data.appId, ingestKey: data.ingestKey };
90
+ } catch {
91
+ return null;
92
+ }
93
+ };
94
+
95
+ const askTelemetryConsent = async (): Promise<TelemetryConsent> => {
96
+ if (!input.isTTY || !output.isTTY) {
97
+ return "no";
98
+ }
99
+
100
+ const rl = createInterface({ input, output });
101
+ const answer = await rl.question("Share anonymous usage stats to help improve Colonel? (y/N): ");
102
+ rl.close();
103
+
104
+ return normalizeConsent(answer) ?? "no";
105
+ };
106
+
107
+ const configureTelemetryEnv = (
108
+ targetDir: string,
109
+ consent: TelemetryConsent,
110
+ endpoint: string,
111
+ appId?: string,
112
+ apiKey?: string
113
+ ): void => {
114
+ const envPath = resolve(targetDir, ".env");
115
+ const existing = existsSync(envPath) ? readFileSync(envPath, "utf8") : "";
116
+
117
+ let content = upsertEnv(existing, "COLONEL_TELEMETRY_ENABLED", consent === "yes" ? "true" : "false");
118
+ content = upsertEnv(content, "COLONEL_TELEMETRY_ENDPOINT", endpoint);
119
+ content = upsertEnv(content, "COLONEL_TELEMETRY_APP_ID", appId ?? "");
120
+ content = upsertEnv(content, "COLONEL_TELEMETRY_KEY", apiKey ?? "");
121
+ writeFileSync(envPath, content);
122
+ };
5
123
 
6
124
  const args = process.argv.slice(2);
7
125
  const targetArg = args.find((arg) => !arg.startsWith("-"));
8
126
  const skipInstall = args.includes("--skip-install");
9
127
  const showHelp = args.includes("--help") || args.includes("-h");
128
+ const telemetryFlag = parseOptionValue(args, "--telemetry");
129
+ const telemetryConsentFromFlag = normalizeConsent(telemetryFlag);
130
+ const telemetryEndpoint = parseOptionValue(args, "--telemetry-endpoint") ?? process.env.COLONEL_TELEMETRY_ENDPOINT ?? DEFAULT_TELEMETRY_ENDPOINT;
131
+ const provisionEndpoint = parseOptionValue(args, "--telemetry-provision-endpoint")
132
+ ?? process.env.COLONEL_TELEMETRY_PROVISION_ENDPOINT
133
+ ?? deriveProvisionEndpoint(telemetryEndpoint);
134
+ const provisionToken = parseOptionValue(args, "--telemetry-provision-token")
135
+ ?? process.env.COLONEL_TELEMETRY_PROVISION_TOKEN;
10
136
 
11
137
  if (showHelp) {
12
- console.log("Usage: bunx create-colonel <project-name> [--skip-install]");
138
+ console.log("Usage: bunx create-colonel <project-name> [--skip-install] [--telemetry yes|no] [--telemetry-endpoint <url>] [--telemetry-provision-endpoint <url>] [--telemetry-provision-token <token>]");
13
139
  console.log("\nOptions:");
14
140
  console.log(" --skip-install Scaffold files without running bun install");
141
+ console.log(" --telemetry Set anonymous telemetry consent without interactive prompt");
142
+ console.log(" --telemetry-endpoint Override telemetry ingestion endpoint");
143
+ console.log(" --telemetry-provision-endpoint Override telemetry app provisioning endpoint");
144
+ console.log(" --telemetry-provision-token Bearer token used for telemetry app provisioning");
15
145
  process.exit(0);
16
146
  }
17
147
 
18
148
  if (!targetArg) {
19
- console.error("Usage: bunx create-colonel <project-name> [--skip-install]");
149
+ console.error("Usage: bunx create-colonel <project-name> [--skip-install] [--telemetry yes|no] [--telemetry-endpoint <url>] [--telemetry-provision-endpoint <url>] [--telemetry-provision-token <token>]");
150
+ process.exit(1);
151
+ }
152
+
153
+ if (telemetryFlag && !telemetryConsentFromFlag) {
154
+ console.error("Invalid --telemetry value. Use yes or no.");
20
155
  process.exit(1);
21
156
  }
22
157
 
@@ -46,6 +181,24 @@ if (existsSync(resolve(localFrameworkPath, "package.json"))) {
46
181
 
47
182
  writeFileSync(packageJsonPath, `${JSON.stringify(packageJson, null, 2)}\n`);
48
183
 
184
+ const consent = telemetryConsentFromFlag ?? await askTelemetryConsent();
185
+ let provisionedAppId: string | undefined;
186
+ let provisionedIngestKey: string | undefined;
187
+
188
+ if (consent === "yes" && provisionToken) {
189
+ const provisioned = await provisionTelemetryApp(
190
+ basename(targetDir),
191
+ "create-colonel",
192
+ provisionEndpoint,
193
+ provisionToken
194
+ );
195
+
196
+ provisionedAppId = provisioned?.appId;
197
+ provisionedIngestKey = provisioned?.ingestKey;
198
+ }
199
+
200
+ configureTelemetryEnv(targetDir, consent, telemetryEndpoint, provisionedAppId, provisionedIngestKey);
201
+
49
202
  if (!skipInstall) {
50
203
  console.log("Installing dependencies...");
51
204
  const install = Bun.spawnSync(["bun", "install"], {
@@ -60,6 +213,14 @@ if (!skipInstall) {
60
213
  }
61
214
  }
62
215
 
216
+ if (consent === "yes" && provisionedAppId && provisionedIngestKey) {
217
+ await trackScaffoldEvent({
218
+ projectName: basename(targetDir),
219
+ skipInstall,
220
+ template: "create-colonel",
221
+ }, telemetryEndpoint, provisionedAppId, provisionedIngestKey);
222
+ }
223
+
63
224
  console.log("\nColonel app created successfully.\n");
64
225
  console.log(` cd ${targetArg}`);
65
226
 
@@ -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