@bitmagic/cli 0.1.46-dev.3 → 0.1.46-dev.5
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 +58 -10
- package/dist/auth/session.js +1 -1
- package/dist/auth/session.js.map +1 -1
- package/dist/auth/subscribe-flow.d.ts +10 -2
- package/dist/auth/subscribe-flow.js.map +1 -1
- package/dist/bin.js +6 -2
- package/dist/bin.js.map +1 -1
- package/dist/cli.d.ts +26 -3
- package/dist/cli.js +76 -7
- package/dist/cli.js.map +1 -1
- package/dist/commands/dev.js +9 -0
- package/dist/commands/dev.js.map +1 -1
- package/dist/commands/forge.js +1 -1
- package/dist/commands/forge.js.map +1 -1
- package/dist/commands/generate.js +9 -1
- package/dist/commands/generate.js.map +1 -1
- package/dist/commands/login.js +12 -9
- package/dist/commands/login.js.map +1 -1
- package/dist/commands/reset-account.d.ts +22 -9
- package/dist/commands/reset-account.js +58 -42
- package/dist/commands/reset-account.js.map +1 -1
- package/dist/commands/subscribe.d.ts +12 -0
- package/dist/commands/subscribe.js +12 -11
- package/dist/commands/subscribe.js.map +1 -1
- package/dist/commands/upgrade.d.ts +2 -1
- package/dist/commands/upgrade.js +12 -5
- package/dist/commands/upgrade.js.map +1 -1
- package/dist/commands/whoami.d.ts +8 -0
- package/dist/commands/whoami.js +29 -9
- package/dist/commands/whoami.js.map +1 -1
- package/dist/config/environments.d.ts +14 -0
- package/dist/config/environments.js +22 -0
- package/dist/config/environments.js.map +1 -1
- package/dist/errors.d.ts +10 -1
- package/dist/errors.js +11 -1
- package/dist/errors.js.map +1 -1
- package/dist/forge/run-pipeline.js +1 -1
- package/dist/forge/run-pipeline.js.map +1 -1
- package/dist/forge/stream.js +10 -5
- package/dist/forge/stream.js.map +1 -1
- package/dist/forge/upload-proxy.js +2 -0
- package/dist/forge/upload-proxy.js.map +1 -1
- package/dist/generate/stream.js +10 -5
- package/dist/generate/stream.js.map +1 -1
- package/dist/http/client.d.ts +6 -2
- package/dist/http/client.js +22 -12
- package/dist/http/client.js.map +1 -1
- package/dist/http/spark-refusal.d.ts +11 -0
- package/dist/http/spark-refusal.js +13 -0
- package/dist/http/spark-refusal.js.map +1 -1
- package/dist/publish/client.js +3 -1
- package/dist/publish/client.js.map +1 -1
- package/dist/scaffold/agents-md.d.ts +2 -1
- package/dist/scaffold/agents-md.js +7 -2
- package/dist/scaffold/agents-md.js.map +1 -1
- package/dist/scaffold/engine-download.js +5 -18
- package/dist/scaffold/engine-download.js.map +1 -1
- package/dist/scaffold/project-files.d.ts +3 -2
- package/dist/scaffold/project-files.js +46 -2
- package/dist/scaffold/project-files.js.map +1 -1
- package/dist/scaffold/project.d.ts +30 -0
- package/dist/scaffold/project.js +55 -29
- package/dist/scaffold/project.js.map +1 -1
- package/dist/scaffold/upgrade-project.js +19 -33
- package/dist/scaffold/upgrade-project.js.map +1 -1
- package/dist/telemetry/command-context.d.ts +73 -0
- package/dist/telemetry/command-context.js +114 -0
- package/dist/telemetry/command-context.js.map +1 -0
- package/dist/telemetry/report.d.ts +43 -0
- package/dist/telemetry/report.js +123 -0
- package/dist/telemetry/report.js.map +1 -0
- package/dist/telemetry/request-headers.d.ts +14 -0
- package/dist/telemetry/request-headers.js +48 -0
- package/dist/telemetry/request-headers.js.map +1 -0
- package/dist/update/check.d.ts +14 -0
- package/dist/update/check.js +20 -4
- package/dist/update/check.js.map +1 -1
- package/dist/update/notice.d.ts +59 -1
- package/dist/update/notice.js +75 -2
- package/dist/update/notice.js.map +1 -1
- package/package.json +4 -4
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import * as fs from 'fs';
|
|
2
2
|
import * as path from 'path';
|
|
3
3
|
import { CliError } from '../errors.js';
|
|
4
|
-
import { VENDORED_DIRS,
|
|
4
|
+
import { VENDORED_DIRS, PLATFORM_GENERATED_FILES, copyOptionalEngineEntries, copyServiceWorker, writeProjectFile, } from './project.js';
|
|
5
|
+
import { isEnvironmentName } from '../config/environments.js';
|
|
5
6
|
import { reconcileAgentsMd, AGENTS_MD_FILE } from './agents-md.js';
|
|
6
7
|
import { CLAUDE_SETTINGS_FILE, mergeReloadHook } from './claude-settings.js';
|
|
7
8
|
/**
|
|
@@ -52,39 +53,18 @@ export function upgradeProject(options) {
|
|
|
52
53
|
for (const dir of VENDORED_DIRS) {
|
|
53
54
|
fs.cpSync(path.join(extractedEngineDir, dir), path.join(engineDir, dir), { recursive: true });
|
|
54
55
|
}
|
|
55
|
-
// Copied only when present
|
|
56
|
-
// the
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
// Same allowance, same reason — and this is how a project scaffolded before the licence shipped
|
|
64
|
-
// acquires engine/LICENSE.md: the rmSync above took the old engine/ with it, so upgrading onto a
|
|
65
|
-
// tarball that carries the terms puts them in place without the creator doing anything.
|
|
66
|
-
for (const file of OPTIONAL_VENDORED_FILES) {
|
|
67
|
-
const source = path.join(extractedEngineDir, file);
|
|
68
|
-
if (fs.existsSync(source)) {
|
|
69
|
-
fs.cpSync(source, path.join(engineDir, file));
|
|
70
|
-
}
|
|
71
|
-
}
|
|
56
|
+
// Copied only when present, exactly as at init — upgrading onto a tarball that predates the docs
|
|
57
|
+
// corpus or the licence files must leave a working project, not a half-replaced engine/ and an
|
|
58
|
+
// error. This is also how a project scaffolded before the licence shipped acquires
|
|
59
|
+
// engine/LICENSE.md: the rmSync above took the old engine/ with it, so upgrading onto a tarball
|
|
60
|
+
// that carries the terms puts them in place without the creator doing anything.
|
|
61
|
+
copyOptionalEngineEntries(extractedEngineDir, engineDir);
|
|
72
62
|
replaced.push('engine/');
|
|
73
|
-
|
|
74
|
-
// feature shipped simply won't have it, and upgrade must not fail a project onto that tarball.
|
|
75
|
-
const serviceWorker = path.join(extractedEngineDir, 'sw-cache-buster.js');
|
|
76
|
-
if (fs.existsSync(serviceWorker)) {
|
|
77
|
-
fs.cpSync(serviceWorker, path.join(root, 'sw-cache-buster.js'));
|
|
63
|
+
if (copyServiceWorker(extractedEngineDir, root)) {
|
|
78
64
|
replaced.push('sw-cache-buster.js');
|
|
79
65
|
}
|
|
80
66
|
for (const [name, render] of PLATFORM_GENERATED_FILES) {
|
|
81
|
-
|
|
82
|
-
// The skills nest two directories deep, and a project scaffolded before they existed — or
|
|
83
|
-
// before a newly added one existed — has no such directory. Without this, writeFileSync ENOENTs
|
|
84
|
-
// and the upgrade dies AFTER engine/ has already been replaced. mkdir on an existing directory
|
|
85
|
-
// is a no-op, so the flat entries pay nothing for it.
|
|
86
|
-
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
87
|
-
fs.writeFileSync(filePath, render());
|
|
67
|
+
writeProjectFile(root, name, render());
|
|
88
68
|
replaced.push(name);
|
|
89
69
|
}
|
|
90
70
|
// Merged, never rewritten — the creator's permissions and their own hooks live in this file, and
|
|
@@ -94,8 +74,7 @@ export function upgradeProject(options) {
|
|
|
94
74
|
const existingSettings = fs.existsSync(settingsPath) ? fs.readFileSync(settingsPath, 'utf-8') : undefined;
|
|
95
75
|
const settings = mergeReloadHook(existingSettings);
|
|
96
76
|
if (settings.contents !== null) {
|
|
97
|
-
|
|
98
|
-
fs.writeFileSync(settingsPath, settings.contents);
|
|
77
|
+
writeProjectFile(root, CLAUDE_SETTINGS_FILE, settings.contents);
|
|
99
78
|
replaced.push(CLAUDE_SETTINGS_FILE);
|
|
100
79
|
}
|
|
101
80
|
// Read-modify-write, not re-rendered from renderBitmagicJson: re-rendering would discard any
|
|
@@ -112,8 +91,15 @@ export function upgradeProject(options) {
|
|
|
112
91
|
meta.environment = environmentPinned;
|
|
113
92
|
// AGENTS.md last, because it is the one file here whose treatment depends on reading the stamp
|
|
114
93
|
// out of the metadata above — and whose result is written back into it below.
|
|
94
|
+
//
|
|
95
|
+
// The pin goes in too: the template's CLI-update section names the npm line this project's
|
|
96
|
+
// environment implies, so it is read AFTER the backfill above (a project that just gained a pin
|
|
97
|
+
// gets an AGENTS.md that knows it). An unrecognised hand-edited value renders as unpinned rather
|
|
98
|
+
// than throwing — refusing to refresh the guidance would be a strange way to report a typo, and
|
|
99
|
+
// the environment resolution the command already did says it far better.
|
|
115
100
|
const storedHash = typeof meta.agentsMdHash === 'string' ? meta.agentsMdHash : undefined;
|
|
116
|
-
const
|
|
101
|
+
const pinned = typeof meta.environment === 'string' && isEnvironmentName(meta.environment) ? meta.environment : undefined;
|
|
102
|
+
const agentsMd = reconcileAgentsMd(root, storedHash, pinned);
|
|
117
103
|
if (agentsMd.hashToStore !== undefined)
|
|
118
104
|
meta.agentsMdHash = agentsMd.hashToStore;
|
|
119
105
|
if (agentsMd.action === 'created' || agentsMd.action === 'rewritten')
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"upgrade-project.js","sourceRoot":"","sources":["../../src/scaffold/upgrade-project.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,IAAI,CAAC;AACzB,OAAO,KAAK,IAAI,MAAM,MAAM,CAAC;AAC7B,OAAO,EAAE,QAAQ,EAAE,MAAM,cAAc,CAAC;AACxC,OAAO,EACL,aAAa,EACb,
|
|
1
|
+
{"version":3,"file":"upgrade-project.js","sourceRoot":"","sources":["../../src/scaffold/upgrade-project.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { type Environment } from '../config/environments.js';
|
|
2
|
+
/**
|
|
3
|
+
* What is driving the CLI.
|
|
4
|
+
*
|
|
5
|
+
* - `claude-code` — a Claude Code session (its `CLAUDECODE` env var, verified).
|
|
6
|
+
* - `json` — some agent, unidentified: `--json` is the flag agents pass.
|
|
7
|
+
* - `tty` — a person at a terminal.
|
|
8
|
+
* - `pipe` — a script or CI: no `--json`, no terminal.
|
|
9
|
+
*
|
|
10
|
+
* Add an agent here only once its marker is verified against the real thing; a
|
|
11
|
+
* guessed env var never fires and just misleads whoever reads the table.
|
|
12
|
+
*/
|
|
13
|
+
export type CliDriver = 'claude-code' | 'json' | 'tty' | 'pipe';
|
|
14
|
+
export declare function detectDriver(input: {
|
|
15
|
+
json: boolean;
|
|
16
|
+
env: Record<string, string | undefined>;
|
|
17
|
+
isTTY: boolean;
|
|
18
|
+
}): CliDriver;
|
|
19
|
+
/**
|
|
20
|
+
* `DO_NOT_TRACK` set to anything but `0`/`false`/empty. The convention says
|
|
21
|
+
* `1`; being generous about the spelling is the polite reading of a signal
|
|
22
|
+
* that only ever means "please don't".
|
|
23
|
+
*
|
|
24
|
+
* Lives here rather than in report.ts because it is consulted BEFORE anything
|
|
25
|
+
* is collected — `beginCommand` skips the whole snapshot when it is set — and
|
|
26
|
+
* report.ts already imports this module, so the other direction would close a
|
|
27
|
+
* cycle. Re-exported from report.ts, which is where callers expect it.
|
|
28
|
+
*/
|
|
29
|
+
export declare function isTelemetryDisabled(env: Record<string, string | undefined>): boolean;
|
|
30
|
+
export interface TokenSnapshot {
|
|
31
|
+
accessToken: string;
|
|
32
|
+
/** Epoch milliseconds, as stored. */
|
|
33
|
+
expiresAt: number;
|
|
34
|
+
}
|
|
35
|
+
export interface CommandContext {
|
|
36
|
+
/** As typed, so a report reads the way the creator invoked it: `publish`, `generate skybox`. */
|
|
37
|
+
name: string;
|
|
38
|
+
json: boolean;
|
|
39
|
+
driver: CliDriver;
|
|
40
|
+
/**
|
|
41
|
+
* Where this command is talking to, resolved ONCE here through the same
|
|
42
|
+
* precedence every command uses (`--env` > BITMAGIC_ENV > the project's
|
|
43
|
+
* bitmagic.json > the stored default). `report.ts` reads it rather than
|
|
44
|
+
* resolving again: a second resolution is a second chance to disagree, and
|
|
45
|
+
* one that skipped the project tier would send a project's beacons to a
|
|
46
|
+
* different environment than the work they describe.
|
|
47
|
+
*
|
|
48
|
+
* Null only when resolution threw — an unknown `--env`, or a project whose
|
|
49
|
+
* `environment` field is not a valid name. The command itself fails with
|
|
50
|
+
* that message moments later; there is simply nowhere to report it to.
|
|
51
|
+
*/
|
|
52
|
+
environment: Environment | null;
|
|
53
|
+
/** Epoch milliseconds when the command began. */
|
|
54
|
+
startedAt: number;
|
|
55
|
+
/** See the module header: the stored access token at start, for the one command that removes it. */
|
|
56
|
+
tokenSnapshot: TokenSnapshot | null;
|
|
57
|
+
}
|
|
58
|
+
export interface BeginCommandInput {
|
|
59
|
+
name: string;
|
|
60
|
+
json: boolean;
|
|
61
|
+
explicitEnv?: string;
|
|
62
|
+
env?: Record<string, string | undefined>;
|
|
63
|
+
isTTY?: boolean;
|
|
64
|
+
now?: () => number;
|
|
65
|
+
baseDir?: string;
|
|
66
|
+
/** Where to look for the project's own environment pin; defaults to process.cwd(). */
|
|
67
|
+
cwd?: string;
|
|
68
|
+
}
|
|
69
|
+
export declare function beginCommand(input: BeginCommandInput): CommandContext;
|
|
70
|
+
/** The running command, or null before `beginCommand` — e.g. under a test that calls a module directly. */
|
|
71
|
+
export declare function currentCommand(): CommandContext | null;
|
|
72
|
+
/** Test seam: forget the running command, so one test's context cannot leak into the next. */
|
|
73
|
+
export declare function resetCommandContextForTests(): void;
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which command this process is running — the one piece of state the CLI's usage
|
|
3
|
+
* reporting is built on.
|
|
4
|
+
*
|
|
5
|
+
* One process runs one command, so this is process-scoped: cli.ts's wrapper calls
|
|
6
|
+
* {@link beginCommand} once before the command's `run`, and two readers use it —
|
|
7
|
+
* `request-headers.ts`, which names the command on every api-server call, and
|
|
8
|
+
* `report.ts`, which sends the outcome when the command ends. It is the CLI's
|
|
9
|
+
* equivalent of a request context, and it lives here rather than being threaded
|
|
10
|
+
* through every command signature because there are five separate fetch sites and
|
|
11
|
+
* eighteen commands, and a parameter every one of them must remember to pass is
|
|
12
|
+
* how one of them ends up unlabelled.
|
|
13
|
+
*
|
|
14
|
+
* Nothing here reaches the network. The token snapshot is a file read, and it
|
|
15
|
+
* exists for exactly one command: `logout` removes the stored credentials, so by
|
|
16
|
+
* the time its outcome is reported there is nothing to authenticate the report
|
|
17
|
+
* with — except what was there when it started.
|
|
18
|
+
*/
|
|
19
|
+
import { readCredentials } from '../config/credentials.js';
|
|
20
|
+
import { resolveEnvironmentForCommand } from '../config/environments.js';
|
|
21
|
+
const AGENT_ENV_MARKERS = [['CLAUDECODE', 'claude-code']];
|
|
22
|
+
export function detectDriver(input) {
|
|
23
|
+
for (const [marker, driver] of AGENT_ENV_MARKERS) {
|
|
24
|
+
if (input.env[marker])
|
|
25
|
+
return driver;
|
|
26
|
+
}
|
|
27
|
+
if (input.json)
|
|
28
|
+
return 'json';
|
|
29
|
+
return input.isTTY ? 'tty' : 'pipe';
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* `DO_NOT_TRACK` set to anything but `0`/`false`/empty. The convention says
|
|
33
|
+
* `1`; being generous about the spelling is the polite reading of a signal
|
|
34
|
+
* that only ever means "please don't".
|
|
35
|
+
*
|
|
36
|
+
* Lives here rather than in report.ts because it is consulted BEFORE anything
|
|
37
|
+
* is collected — `beginCommand` skips the whole snapshot when it is set — and
|
|
38
|
+
* report.ts already imports this module, so the other direction would close a
|
|
39
|
+
* cycle. Re-exported from report.ts, which is where callers expect it.
|
|
40
|
+
*/
|
|
41
|
+
export function isTelemetryDisabled(env) {
|
|
42
|
+
const value = env.DO_NOT_TRACK?.trim().toLowerCase();
|
|
43
|
+
return value !== undefined && value !== '' && value !== '0' && value !== 'false';
|
|
44
|
+
}
|
|
45
|
+
let current = null;
|
|
46
|
+
/**
|
|
47
|
+
* The environment this command targets, or null if it cannot be resolved.
|
|
48
|
+
*
|
|
49
|
+
* `resolveEnvironmentForCommand` is the same entry point the commands use, so
|
|
50
|
+
* a project pinned to an environment reports there too. It throws for an
|
|
51
|
+
* unknown name typed right now (a bad `--env`, a bad `bitmagic.json`), which
|
|
52
|
+
* the command surfaces properly — here it only means "no environment to report
|
|
53
|
+
* to", never a failed command.
|
|
54
|
+
*/
|
|
55
|
+
function snapshotEnvironment(input, env) {
|
|
56
|
+
try {
|
|
57
|
+
return resolveEnvironmentForCommand({
|
|
58
|
+
explicit: input.explicitEnv,
|
|
59
|
+
cwd: input.cwd,
|
|
60
|
+
baseDir: input.baseDir,
|
|
61
|
+
env,
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* The stored access token for `environment`, or null.
|
|
70
|
+
*
|
|
71
|
+
* Every failure collapses to null on purpose: no environment, an unreadable
|
|
72
|
+
* credentials file, nothing stored — the command itself will report whichever
|
|
73
|
+
* of those matters, in its own words. This is a snapshot for a report, and a
|
|
74
|
+
* report is never worth failing a command over.
|
|
75
|
+
*/
|
|
76
|
+
function snapshotToken(environment, baseDir) {
|
|
77
|
+
if (environment === null)
|
|
78
|
+
return null;
|
|
79
|
+
try {
|
|
80
|
+
const stored = readCredentials(environment.name, baseDir);
|
|
81
|
+
return stored ? { accessToken: stored.accessToken, expiresAt: stored.expiresAt } : null;
|
|
82
|
+
}
|
|
83
|
+
catch {
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
export function beginCommand(input) {
|
|
88
|
+
const env = input.env ?? process.env;
|
|
89
|
+
// Nothing is collected for a creator who opted out. Both snapshots below are
|
|
90
|
+
// filesystem work — a walk up the tree for the project's bitmagic.json, then
|
|
91
|
+
// credentials.json — done on every one of the eighteen commands, including
|
|
92
|
+
// the offline ones and the `reload` hook that runs after every agent turn.
|
|
93
|
+
// None of it has a purpose if no report will be sent.
|
|
94
|
+
const collect = !isTelemetryDisabled(env);
|
|
95
|
+
const environment = collect ? snapshotEnvironment(input, env) : null;
|
|
96
|
+
current = {
|
|
97
|
+
name: input.name,
|
|
98
|
+
json: input.json,
|
|
99
|
+
driver: detectDriver({ json: input.json, env, isTTY: input.isTTY ?? process.stdout.isTTY === true }),
|
|
100
|
+
environment,
|
|
101
|
+
startedAt: (input.now ?? Date.now)(),
|
|
102
|
+
tokenSnapshot: collect ? snapshotToken(environment, input.baseDir) : null,
|
|
103
|
+
};
|
|
104
|
+
return current;
|
|
105
|
+
}
|
|
106
|
+
/** The running command, or null before `beginCommand` — e.g. under a test that calls a module directly. */
|
|
107
|
+
export function currentCommand() {
|
|
108
|
+
return current;
|
|
109
|
+
}
|
|
110
|
+
/** Test seam: forget the running command, so one test's context cannot leak into the next. */
|
|
111
|
+
export function resetCommandContextForTests() {
|
|
112
|
+
current = null;
|
|
113
|
+
}
|
|
114
|
+
//# sourceMappingURL=command-context.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"command-context.js","sourceRoot":"","sources":["../../src/telemetry/command-context.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { isTelemetryDisabled } from './command-context.js';
|
|
2
|
+
export { isTelemetryDisabled };
|
|
3
|
+
/**
|
|
4
|
+
* Cap on each request the report makes (the token refresh, when one is due, and
|
|
5
|
+
* the POST itself). A pending fetch keeps Node alive, so an unbounded one would
|
|
6
|
+
* hold a finished command open for as long as the network sulks.
|
|
7
|
+
*/
|
|
8
|
+
export declare const REPORT_TIMEOUT_MS = 1500;
|
|
9
|
+
export declare const CLI_EVENTS_PATH = "/api/cli/v1/events";
|
|
10
|
+
export interface CommandReport {
|
|
11
|
+
/** `finished` for every command's end; `started` for a long-running command once it is up. */
|
|
12
|
+
stage: 'started' | 'finished';
|
|
13
|
+
ok?: boolean;
|
|
14
|
+
durationMs?: number;
|
|
15
|
+
exitCode?: number;
|
|
16
|
+
/** A category — `CliError.code`, `cli_error`, `crash:TypeError` — never a message. */
|
|
17
|
+
failure?: string;
|
|
18
|
+
}
|
|
19
|
+
export interface ReportDeps {
|
|
20
|
+
fetch: typeof globalThis.fetch;
|
|
21
|
+
env: Record<string, string | undefined>;
|
|
22
|
+
now?: () => number;
|
|
23
|
+
/** Where credentials live; tests point this at a temp dir. */
|
|
24
|
+
baseDir?: string;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* The failure category for an error that is not a CliError — a genuine bug, not
|
|
28
|
+
* a refusal we wrote a message for. `crash:TypeError`, `crash:RangeError`: the
|
|
29
|
+
* error's class, which says what kind of bug without saying anything about the
|
|
30
|
+
* creator's machine. Anything odd in the name is folded to `_` so the value
|
|
31
|
+
* always fits the shape api-server accepts.
|
|
32
|
+
*/
|
|
33
|
+
export declare function crashCategory(error: unknown): string;
|
|
34
|
+
/**
|
|
35
|
+
* Report the running command's `report` to api-server. Resolves when the report
|
|
36
|
+
* has been sent, refused, or given up on — always, and never by throwing.
|
|
37
|
+
*
|
|
38
|
+
* Callers `await` it (bounded, see above) rather than fire and forget: on the
|
|
39
|
+
* failure path cli.ts calls `process.exit` next, which would otherwise cut the
|
|
40
|
+
* request off mid-flight, and on the success path the pending fetch would keep
|
|
41
|
+
* the process alive for the same time either way.
|
|
42
|
+
*/
|
|
43
|
+
export declare function reportCommandEvent(report: CommandReport, deps?: ReportDeps): Promise<void>;
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The command beacon: one `POST /api/cli/v1/events` per command, saying how it went.
|
|
3
|
+
*
|
|
4
|
+
* Why it exists: api-server can see the commands that call it — but `dev`,
|
|
5
|
+
* `build`, `check`, `verify` and `reload` are offline by design, and even a
|
|
6
|
+
* `publish` that fails on a stale verify never reaches the server. This is how
|
|
7
|
+
* every command becomes visible, and how the ones the server does see get an
|
|
8
|
+
* end-to-end outcome and duration to go with the server's own event. The
|
|
9
|
+
* server turns it into `cli_command` / `cli_command_started` (see
|
|
10
|
+
* `api-server/src/cli/cli-analytics.ts`); nothing goes to Google from here.
|
|
11
|
+
*
|
|
12
|
+
* Contract, in order of importance:
|
|
13
|
+
*
|
|
14
|
+
* 1. It never fails or delays a command. Every failure path — not logged in,
|
|
15
|
+
* no network, a slow server, a bad `--env` — returns silently. It is
|
|
16
|
+
* bounded to {@link REPORT_TIMEOUT_MS} per request so a finished command is
|
|
17
|
+
* never held open by a sulking network (the same reasoning as
|
|
18
|
+
* update/check.ts). It never prints.
|
|
19
|
+
* 2. It sends categories, never messages: the command name, ok/failed, the
|
|
20
|
+
* exit code, a `CliError.code`, and a duration. A message can name a path
|
|
21
|
+
* or a game; none of that belongs in an analytics event.
|
|
22
|
+
* 3. It is authenticated with the creator's own token, so the server can key
|
|
23
|
+
* the event to the account and look up their real Pro state. No token, no
|
|
24
|
+
* report — an anonymous beacon would be a spammable endpoint for nothing.
|
|
25
|
+
* 4. `DO_NOT_TRACK=1` (the cross-tool convention, consoledonottrack.com)
|
|
26
|
+
* turns it off. It does not touch the ordinary api-server calls a command
|
|
27
|
+
* makes — those are the command's work, and the server logs them anyway.
|
|
28
|
+
*/
|
|
29
|
+
import { resolveAuth0ClientId } from '../config/environments.js';
|
|
30
|
+
import { getAccessToken } from '../auth/session.js';
|
|
31
|
+
import { apiServerHeaders } from './request-headers.js';
|
|
32
|
+
import { currentCommand, isTelemetryDisabled } from './command-context.js';
|
|
33
|
+
// Re-exported because this is where the opt-out is enforced for a report that is already
|
|
34
|
+
// under way; `beginCommand` consults the same predicate to skip collecting anything at all.
|
|
35
|
+
export { isTelemetryDisabled };
|
|
36
|
+
/**
|
|
37
|
+
* Cap on each request the report makes (the token refresh, when one is due, and
|
|
38
|
+
* the POST itself). A pending fetch keeps Node alive, so an unbounded one would
|
|
39
|
+
* hold a finished command open for as long as the network sulks.
|
|
40
|
+
*/
|
|
41
|
+
export const REPORT_TIMEOUT_MS = 1500;
|
|
42
|
+
export const CLI_EVENTS_PATH = '/api/cli/v1/events';
|
|
43
|
+
/**
|
|
44
|
+
* The failure category for an error that is not a CliError — a genuine bug, not
|
|
45
|
+
* a refusal we wrote a message for. `crash:TypeError`, `crash:RangeError`: the
|
|
46
|
+
* error's class, which says what kind of bug without saying anything about the
|
|
47
|
+
* creator's machine. Anything odd in the name is folded to `_` so the value
|
|
48
|
+
* always fits the shape api-server accepts.
|
|
49
|
+
*/
|
|
50
|
+
export function crashCategory(error) {
|
|
51
|
+
const name = error instanceof Error && error.name ? error.name : 'unknown';
|
|
52
|
+
return `crash:${name.replace(/[^A-Za-z0-9_.-]/g, '_').slice(0, 40)}`;
|
|
53
|
+
}
|
|
54
|
+
/** The given fetch, with every call bounded by REPORT_TIMEOUT_MS. */
|
|
55
|
+
function boundedFetch(fetchImpl) {
|
|
56
|
+
return (input, init) => fetchImpl(input, { ...init, signal: AbortSignal.timeout(REPORT_TIMEOUT_MS) });
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* The token to authenticate the report with, or null for "no report".
|
|
60
|
+
*
|
|
61
|
+
* The stored token first — refreshed if it has expired, exactly as any command
|
|
62
|
+
* would, so a report never wastes a request on a 401 — and failing that the
|
|
63
|
+
* snapshot taken when the command began, which is what `logout` leaves behind.
|
|
64
|
+
*/
|
|
65
|
+
async function resolveReportToken(environment, command, deps) {
|
|
66
|
+
try {
|
|
67
|
+
const clientId = resolveAuth0ClientId(environment, deps.env);
|
|
68
|
+
return await getAccessToken(environment, clientId, {
|
|
69
|
+
fetch: boundedFetch(deps.fetch),
|
|
70
|
+
sleep: (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
|
|
71
|
+
now: deps.now,
|
|
72
|
+
}, deps.baseDir);
|
|
73
|
+
}
|
|
74
|
+
catch {
|
|
75
|
+
const snapshot = command.tokenSnapshot;
|
|
76
|
+
const now = (deps.now ?? Date.now)();
|
|
77
|
+
return snapshot !== null && snapshot.expiresAt > now ? snapshot.accessToken : null;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
function defaultDeps() {
|
|
81
|
+
return { fetch: globalThis.fetch, env: process.env };
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Report the running command's `report` to api-server. Resolves when the report
|
|
85
|
+
* has been sent, refused, or given up on — always, and never by throwing.
|
|
86
|
+
*
|
|
87
|
+
* Callers `await` it (bounded, see above) rather than fire and forget: on the
|
|
88
|
+
* failure path cli.ts calls `process.exit` next, which would otherwise cut the
|
|
89
|
+
* request off mid-flight, and on the success path the pending fetch would keep
|
|
90
|
+
* the process alive for the same time either way.
|
|
91
|
+
*/
|
|
92
|
+
export async function reportCommandEvent(report, deps = defaultDeps()) {
|
|
93
|
+
try {
|
|
94
|
+
if (isTelemetryDisabled(deps.env))
|
|
95
|
+
return;
|
|
96
|
+
const command = currentCommand();
|
|
97
|
+
if (command === null)
|
|
98
|
+
return;
|
|
99
|
+
// Resolved once, at command start — see CommandContext.environment for why
|
|
100
|
+
// this must not resolve again. Null means it could not be resolved at all,
|
|
101
|
+
// so there is nowhere to send this.
|
|
102
|
+
const environment = command.environment;
|
|
103
|
+
if (environment === null)
|
|
104
|
+
return;
|
|
105
|
+
const token = await resolveReportToken(environment, command, deps);
|
|
106
|
+
if (token === null)
|
|
107
|
+
return;
|
|
108
|
+
await boundedFetch(deps.fetch)(`${environment.apiUrl}${CLI_EVENTS_PATH}`, {
|
|
109
|
+
method: 'POST',
|
|
110
|
+
headers: {
|
|
111
|
+
...apiServerHeaders(),
|
|
112
|
+
Authorization: `Bearer ${token}`,
|
|
113
|
+
'Content-Type': 'application/json',
|
|
114
|
+
},
|
|
115
|
+
body: JSON.stringify({ command: command.name, ...report }),
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
catch {
|
|
119
|
+
// Best-effort by design — see the module header. Whatever went wrong here,
|
|
120
|
+
// the command's own outcome has already been decided and printed.
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
//# sourceMappingURL=report.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"report.js","sourceRoot":"","sources":["../../src/telemetry/report.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export declare const CLI_COMMAND_HEADER = "X-Bitmagic-Cli-Command";
|
|
2
|
+
export declare const CLI_DRIVER_HEADER = "X-Bitmagic-Cli-Driver";
|
|
3
|
+
/** `bitmagic-cli/0.1.46 (darwin-arm64; node/v22.12.0)` — the shape api-server's context reader parses. */
|
|
4
|
+
export declare function userAgent(): string;
|
|
5
|
+
/**
|
|
6
|
+
* Spread these into the `headers` of every fetch aimed at api-server. Callers
|
|
7
|
+
* add their own `Authorization`, `Accept` and `Content-Type` after — nothing
|
|
8
|
+
* here collides with those.
|
|
9
|
+
*
|
|
10
|
+
* The command headers are present only once cli.ts has begun a command; a
|
|
11
|
+
* module exercised directly (a test, a future library caller) still identifies
|
|
12
|
+
* the CLI, just not the command.
|
|
13
|
+
*/
|
|
14
|
+
export declare function apiServerHeaders(): Record<string, string>;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The headers every api-server call carries, so the server's analytics know who
|
|
3
|
+
* asked and why.
|
|
4
|
+
*
|
|
5
|
+
* The CLI never sends analytics to Google itself — the GA4 Measurement Protocol
|
|
6
|
+
* needs an API secret, and a public npm package cannot keep one. api-server
|
|
7
|
+
* emits every event instead (`api-server/src/cli/cli-analytics.ts` reads these
|
|
8
|
+
* headers back as `cli_version`, `cli_platform`, `cli_node`, `cli_command` and
|
|
9
|
+
* `cli_driver`). That is what lets a `cli_asset_generated` event tell `generate
|
|
10
|
+
* image` from `cover`, which share a route, and a `publish` from a `forge`,
|
|
11
|
+
* which share the signed-URL route.
|
|
12
|
+
*
|
|
13
|
+
* Only for api-server. GCS signed URLs and the npm registry get nothing from
|
|
14
|
+
* here — a signed PUT can refuse headers it did not sign for, and neither has
|
|
15
|
+
* any use for them.
|
|
16
|
+
*/
|
|
17
|
+
import { currentCliVersion } from '../update/cli-version.js';
|
|
18
|
+
import { currentCommand } from './command-context.js';
|
|
19
|
+
export const CLI_COMMAND_HEADER = 'X-Bitmagic-Cli-Command';
|
|
20
|
+
export const CLI_DRIVER_HEADER = 'X-Bitmagic-Cli-Driver';
|
|
21
|
+
let cachedUserAgent = null;
|
|
22
|
+
/** `bitmagic-cli/0.1.46 (darwin-arm64; node/v22.12.0)` — the shape api-server's context reader parses. */
|
|
23
|
+
export function userAgent() {
|
|
24
|
+
if (cachedUserAgent === null) {
|
|
25
|
+
const version = currentCliVersion() || 'unknown';
|
|
26
|
+
cachedUserAgent = `bitmagic-cli/${version} (${process.platform}-${process.arch}; node/${process.version})`;
|
|
27
|
+
}
|
|
28
|
+
return cachedUserAgent;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Spread these into the `headers` of every fetch aimed at api-server. Callers
|
|
32
|
+
* add their own `Authorization`, `Accept` and `Content-Type` after — nothing
|
|
33
|
+
* here collides with those.
|
|
34
|
+
*
|
|
35
|
+
* The command headers are present only once cli.ts has begun a command; a
|
|
36
|
+
* module exercised directly (a test, a future library caller) still identifies
|
|
37
|
+
* the CLI, just not the command.
|
|
38
|
+
*/
|
|
39
|
+
export function apiServerHeaders() {
|
|
40
|
+
const headers = { 'User-Agent': userAgent() };
|
|
41
|
+
const command = currentCommand();
|
|
42
|
+
if (command) {
|
|
43
|
+
headers[CLI_COMMAND_HEADER] = command.name;
|
|
44
|
+
headers[CLI_DRIVER_HEADER] = command.driver;
|
|
45
|
+
}
|
|
46
|
+
return headers;
|
|
47
|
+
}
|
|
48
|
+
//# sourceMappingURL=request-headers.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"request-headers.js","sourceRoot":"","sources":["../../src/telemetry/request-headers.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AACH,OAAO,EAAE,iBAAiB,EAAE,MAAM,0BAA0B,CAAC;AAC7D,OAAO,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAEtD,MAAM,CAAC,MAAM,kBAAkB,GAAG,wBAAwB,CAAC;AAC3D,MAAM,CAAC,MAAM,iBAAiB,GAAG,uBAAuB,CAAC;AAEzD,IAAI,eAAe,GAAkB,IAAI,CAAC;AAE1C,0GAA0G;AAC1G,MAAM,UAAU,SAAS;IACvB,IAAI,eAAe,KAAK,IAAI,EAAE,CAAC;QAC7B,MAAM,OAAO,GAAG,iBAAiB,EAAE,IAAI,SAAS,CAAC;QACjD,eAAe,GAAG,gBAAgB,OAAO,KAAK,OAAO,CAAC,QAAQ,IAAI,OAAO,CAAC,IAAI,UAAU,OAAO,CAAC,OAAO,GAAG,CAAC;IAC7G,CAAC;IACD,OAAO,eAAe,CAAC;AACzB,CAAC;AAED;;;;;;;;GAQG;AACH,MAAM,UAAU,gBAAgB;IAC9B,MAAM,OAAO,GAA2B,EAAE,YAAY,EAAE,SAAS,EAAE,EAAE,CAAC;IACtE,MAAM,OAAO,GAAG,cAAc,EAAE,CAAC;IACjC,IAAI,OAAO,EAAE,CAAC;QACZ,OAAO,CAAC,kBAAkB,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC;QAC3C,OAAO,CAAC,iBAAiB,CAAC,GAAG,OAAO,CAAC,MAAM,CAAC;IAC9C,CAAC;IACD,OAAO,OAAO,CAAC;AACjB,CAAC"}
|
package/dist/update/check.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { EnvironmentName } from '../config/environments.js';
|
|
1
2
|
import { type UpdateCheckCache, type UpdateTag } from './notice.js';
|
|
2
3
|
export declare function updateCachePath(baseDir?: string): string;
|
|
3
4
|
/**
|
|
@@ -24,6 +25,19 @@ export declare function fetchTagVersion(tag: UpdateTag, fetchImpl?: typeof globa
|
|
|
24
25
|
* every command wait on npm to be told something it could just as well be told tomorrow.
|
|
25
26
|
*/
|
|
26
27
|
export declare function pendingUpdateNotice(currentVersion: string, baseDir?: string): string | null;
|
|
28
|
+
/**
|
|
29
|
+
* The single line bin.ts prints before a command runs — at most one install command, ever.
|
|
30
|
+
*
|
|
31
|
+
* The wrong-line notice wins over the newer-version one, and suppresses it. Both at once would
|
|
32
|
+
* print two different `npm i -g` commands in the same breath ("update to 0.1.45 on your line" next
|
|
33
|
+
* to "be on the other line"), which is the ambiguity this exists to end: a creator standing in a
|
|
34
|
+
* project should never have to choose which one they meant.
|
|
35
|
+
*
|
|
36
|
+
* `projectEnvironment` is the project's committed pin, or undefined outside a project and for one
|
|
37
|
+
* that has none. Undefined leaves the old behaviour exactly as it was: nothing here invents a line
|
|
38
|
+
* for a directory that never claimed one.
|
|
39
|
+
*/
|
|
40
|
+
export declare function startupNotice(currentVersion: string, projectEnvironment: EnvironmentName | undefined, baseDir?: string): string | null;
|
|
27
41
|
/**
|
|
28
42
|
* Refresh the cache if it is stale. Fire-and-forget: callers must not await this.
|
|
29
43
|
*
|
package/dist/update/check.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import * as fs from 'fs';
|
|
2
2
|
import * as path from 'path';
|
|
3
3
|
import { defaultBaseDir } from '../config/credentials.js';
|
|
4
|
-
import { cliUpdateNotice, isCacheStale, updateTagFor, } from './notice.js';
|
|
4
|
+
import { cliLineMismatchNotice, cliUpdateNotice, isCacheStale, updateTagFor, } from './notice.js';
|
|
5
5
|
/** Public registry metadata, one document per dist-tag. No auth, no scope needed. */
|
|
6
6
|
const NPM_PACKAGE_URL = 'https://registry.npmjs.org/@bitmagic/cli';
|
|
7
7
|
/**
|
|
@@ -90,6 +90,22 @@ export function pendingUpdateNotice(currentVersion, baseDir) {
|
|
|
90
90
|
return null;
|
|
91
91
|
return cliUpdateNotice(currentVersion, cache.latest);
|
|
92
92
|
}
|
|
93
|
+
/**
|
|
94
|
+
* The single line bin.ts prints before a command runs — at most one install command, ever.
|
|
95
|
+
*
|
|
96
|
+
* The wrong-line notice wins over the newer-version one, and suppresses it. Both at once would
|
|
97
|
+
* print two different `npm i -g` commands in the same breath ("update to 0.1.45 on your line" next
|
|
98
|
+
* to "be on the other line"), which is the ambiguity this exists to end: a creator standing in a
|
|
99
|
+
* project should never have to choose which one they meant.
|
|
100
|
+
*
|
|
101
|
+
* `projectEnvironment` is the project's committed pin, or undefined outside a project and for one
|
|
102
|
+
* that has none. Undefined leaves the old behaviour exactly as it was: nothing here invents a line
|
|
103
|
+
* for a directory that never claimed one.
|
|
104
|
+
*/
|
|
105
|
+
export function startupNotice(currentVersion, projectEnvironment, baseDir) {
|
|
106
|
+
const mismatch = projectEnvironment === undefined ? null : cliLineMismatchNotice(currentVersion, projectEnvironment);
|
|
107
|
+
return mismatch ?? pendingUpdateNotice(currentVersion, baseDir);
|
|
108
|
+
}
|
|
93
109
|
/**
|
|
94
110
|
* Refresh the cache if it is stale. Fire-and-forget: callers must not await this.
|
|
95
111
|
*
|
|
@@ -100,12 +116,12 @@ export async function refreshUpdateCacheInBackground(currentVersion, now = Date.
|
|
|
100
116
|
const tag = updateTagFor(currentVersion);
|
|
101
117
|
const cache = readUpdateCache(baseDir);
|
|
102
118
|
// A cache about the other line is refreshed immediately however recent it is — switching lines
|
|
103
|
-
// with `npm i -g @bitmagic/cli@dev` must not leave the nudge mute for
|
|
119
|
+
// with `npm i -g @bitmagic/cli@dev` must not leave the nudge mute for the rest of its interval.
|
|
104
120
|
if (cache !== null && cache.tag === tag && !isCacheStale(cache, now))
|
|
105
121
|
return;
|
|
106
122
|
const latest = await fetchTagVersion(tag, fetchImpl);
|
|
107
|
-
// Recorded even when the fetch failed, so an offline machine backs off for
|
|
108
|
-
// retrying npm on every single command.
|
|
123
|
+
// Recorded even when the fetch failed, so an offline machine backs off for the interval rather
|
|
124
|
+
// than retrying npm on every single command.
|
|
109
125
|
writeUpdateCache({ latest, tag, checkedAt: now }, baseDir);
|
|
110
126
|
}
|
|
111
127
|
//# sourceMappingURL=check.js.map
|
package/dist/update/check.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"check.js","sourceRoot":"","sources":["../../src/update/check.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,IAAI,CAAC;AACzB,OAAO,KAAK,IAAI,MAAM,MAAM,CAAC;AAC7B,OAAO,EAAE,cAAc,EAAE,MAAM,0BAA0B,CAAC;
|
|
1
|
+
{"version":3,"file":"check.js","sourceRoot":"","sources":["../../src/update/check.ts"],"names":[],"mappings":"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"}
|