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 +27 -0
- package/package.json +1 -1
- package/src/cli.ts +184 -2
- package/template/package.json +2 -2
- package/template/src/bootstrap/server.ts +14 -1
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
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
|
|
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
|
|