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 +17 -0
- package/package.json +1 -1
- package/src/cli.ts +163 -2
- package/template/package.json +2 -2
- package/template/src/bootstrap/server.ts +14 -1
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
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
|
|
package/template/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "colonel-app",
|
|
3
|
-
"version": "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.
|
|
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
|
|