dbdiagram 0.2.0 → 0.3.0
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 +13 -0
- package/dist/actions/build/build-document.action.js +10 -2
- package/dist/actions/push.action.js +12 -0
- package/dist/commands/auth/auth.command.js +1 -1
- package/dist/commands/build/build.command.js +1 -1
- package/dist/commands/tokens/tokens.command.js +1 -1
- package/dist/config/credential-manager.js +10 -23
- package/dist/config/settings-manager.js +14 -31
- package/dist/config.js +4 -0
- package/dist/constants/dot-dbdiagram-dir.constant.js +1 -0
- package/dist/hooks/global.hook.js +8 -1
- package/dist/index.js +22 -2
- package/dist/integrations/telemetry/telemetry.integration.js +77 -0
- package/dist/program.js +2 -1
- package/dist/services/telemetry.service.js +59 -0
- package/dist/types/telemetry.type.js +1 -0
- package/dist/utils/agent-detector.util.js +27 -0
- package/dist/utils/command-name.util.js +12 -0
- package/dist/utils/environment-detector.util.js +6 -0
- package/dist/utils/file.util.js +31 -0
- package/package.json +3 -1
package/README.md
CHANGED
|
@@ -181,6 +181,19 @@ yarn build # compile to dist/
|
|
|
181
181
|
yarn start # run compiled output
|
|
182
182
|
```
|
|
183
183
|
|
|
184
|
+
### Telemetry in development
|
|
185
|
+
|
|
186
|
+
Copy `.env.example` to `.env`. Its defaults point telemetry at a local collector
|
|
187
|
+
(`TRACKING_COLLECTOR_URL=http://localhost:9090`) and tag events with a non-production app id (`TRACKING_APP_ID=dbdiagram-cli-test`), so development never writes to production analytics.
|
|
188
|
+
|
|
189
|
+
To disable tracking entirely, point the collector at a local address with nothing listening:
|
|
190
|
+
|
|
191
|
+
```sh
|
|
192
|
+
TRACKING_COLLECTOR_URL=http://localhost:9090 yarn dev push ...
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
Sends fail silently (the CLI never retries and never surfaces telemetry errors), so no events leave your machine. To instead inspect what would be sent, run [Snowplow Micro](https://docs.snowplow.io/docs/data-product-studio/data-quality/snowplow-micro/) on that port and read `curl localhost:9090/micro/good` (passed validation) and `/micro/bad` (failed).
|
|
196
|
+
|
|
184
197
|
## Test
|
|
185
198
|
|
|
186
199
|
```sh
|
|
@@ -6,6 +6,7 @@ import { DOCUMENT_SOURCE_TYPE } from '../../constants/document.constant.js';
|
|
|
6
6
|
import { AUTH_NOT_LOGGED_IN, DBML_PARSE_ERROR, FILE_READ_ERROR, PROJECT_NAME_INVALID, PROJECT_NAME_MISSING, SOURCE_MISSING, VERSION_NAME_INVALID, WORKSPACE_NAME_INVALID, WORKSPACE_NAME_MISSING, } from '../../constants/cli-error.constant.js';
|
|
7
7
|
import { CliError } from '../../errors/cli.error.js';
|
|
8
8
|
import { portalIntegration } from '../../integrations/portal/portal.integration.js';
|
|
9
|
+
import { trackEvent } from '../../services/telemetry.service.js';
|
|
9
10
|
import { prepareBuildDocumentData } from '../../services/dbml/dbml.service.js';
|
|
10
11
|
import { getCliErrorMessageFromCode, toCliError } from '../../utils/cli-error.util.js';
|
|
11
12
|
import { formatDbmlError, getDbmlVersion } from '../../utils/dbml.util.js';
|
|
@@ -88,6 +89,7 @@ export async function documentAction(options) {
|
|
|
88
89
|
const s = spinner();
|
|
89
90
|
s.start('Building document on dbdocs...');
|
|
90
91
|
try {
|
|
92
|
+
const currentUser = await portalIntegration.getCurrentUser();
|
|
91
93
|
const { project } = await portalIntegration.createDocument({
|
|
92
94
|
projectName: options.projectUrlOrName,
|
|
93
95
|
projectDescription: preparedDocumentData.description,
|
|
@@ -101,10 +103,16 @@ export async function documentAction(options) {
|
|
|
101
103
|
normalizedDatabase: preparedDocumentData.normalizedDatabase,
|
|
102
104
|
dbmlVersion: getDbmlVersion(),
|
|
103
105
|
});
|
|
104
|
-
const
|
|
106
|
+
const { workspaceUrl: workspaceUrlName, urlName: projectUrlName } = project;
|
|
107
|
+
trackEvent({
|
|
108
|
+
name: 'cli_document_build',
|
|
109
|
+
properties: { workspaceUrlName, projectUrlName },
|
|
110
|
+
userId: currentUser.id.toString(),
|
|
111
|
+
});
|
|
112
|
+
const url = `${DBDOCS_CONFIGS.baseUrl}/${workspaceUrlName}/${projectUrlName}`;
|
|
105
113
|
if (options.json) {
|
|
106
114
|
s.stop();
|
|
107
|
-
printJson({ projectUrlName: `${
|
|
115
|
+
printJson({ projectUrlName: `${workspaceUrlName}/${projectUrlName}`, url });
|
|
108
116
|
}
|
|
109
117
|
else {
|
|
110
118
|
s.stop(`Document built successfully. Visit: ${url}`);
|
|
@@ -2,6 +2,7 @@ import { readFile } from 'node:fs/promises';
|
|
|
2
2
|
import { spinner } from '@clack/prompts';
|
|
3
3
|
import { getCredential } from '../config/credential-manager.js';
|
|
4
4
|
import { portalIntegration } from '../integrations/portal/portal.integration.js';
|
|
5
|
+
import { trackEvent } from '../services/telemetry.service.js';
|
|
5
6
|
import { deriveDiagramVizPathFromDBML, readDiagramVizFile } from '../services/viz/diagram-viz-file.service.js';
|
|
6
7
|
import { toServerFormat } from '../services/viz/diagram-viz-converter.service.js';
|
|
7
8
|
import { preparePushDiagramData } from '../services/dbml/dbml.service.js';
|
|
@@ -49,16 +50,27 @@ export async function pushAction(filepath, options, thisCommand) {
|
|
|
49
50
|
const s = spinner();
|
|
50
51
|
s.start('Pushing diagram to dbdiagram...');
|
|
51
52
|
try {
|
|
53
|
+
const currentUser = await portalIntegration.getCurrentUser();
|
|
52
54
|
let diagramId;
|
|
53
55
|
if (!options.new && options.diagramId) {
|
|
54
56
|
({ diagramId } = await portalIntegration.updateDiagram({
|
|
55
57
|
diagramId: options.diagramId, name: options.name, dbml, vizData,
|
|
56
58
|
}));
|
|
59
|
+
trackEvent({
|
|
60
|
+
name: 'cli_diagram_push',
|
|
61
|
+
properties: { action: 'update', diagramId, workspaceId: options.workspace ?? null },
|
|
62
|
+
userId: currentUser.id.toString(),
|
|
63
|
+
});
|
|
57
64
|
}
|
|
58
65
|
else {
|
|
59
66
|
({ diagramId } = await portalIntegration.createDiagram({
|
|
60
67
|
name: options.name, dbml, workspaceId: options.workspace, vizData,
|
|
61
68
|
}));
|
|
69
|
+
trackEvent({
|
|
70
|
+
name: 'cli_diagram_push',
|
|
71
|
+
properties: { action: 'create', diagramId, workspaceId: options.workspace ?? null },
|
|
72
|
+
userId: currentUser.id.toString(),
|
|
73
|
+
});
|
|
62
74
|
}
|
|
63
75
|
const diagramUrl = `${DBDIAGRAM_CONFIGS.baseUrl}/d/${diagramId}`;
|
|
64
76
|
if (options.json) {
|
|
@@ -8,7 +8,7 @@ export function registerAuthCommand(program) {
|
|
|
8
8
|
.addHelpText('after', '\nAlternatively, set the DBDIAGRAM_TOKEN env var to authenticate with a\n' +
|
|
9
9
|
'CLI token (see: dbdiagram tokens generate).')
|
|
10
10
|
.action(() => {
|
|
11
|
-
auth.
|
|
11
|
+
auth.outputHelp();
|
|
12
12
|
});
|
|
13
13
|
registerLoginCommand(auth);
|
|
14
14
|
registerLogoutCommand(auth);
|
|
@@ -7,7 +7,7 @@ export function registerTokensCommand(program) {
|
|
|
7
7
|
.description('Manage your CLI tokens')
|
|
8
8
|
.addHelpText('after', '\nSet a token value as DBDIAGRAM_TOKEN env var to authenticate without browser login.')
|
|
9
9
|
.action(() => {
|
|
10
|
-
tokens.
|
|
10
|
+
tokens.outputHelp();
|
|
11
11
|
});
|
|
12
12
|
registerTokenGenerateCommand(tokens);
|
|
13
13
|
registerTokenListCommand(tokens);
|
|
@@ -1,40 +1,27 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
|
-
import
|
|
3
|
-
import path from 'node:path';
|
|
4
|
-
import { DOT_DBDIAGRAM_DIR, CREDENTIALS_FILE_NAME } from '../constants/dot-dbdiagram-dir.constant.js';
|
|
2
|
+
import { getGlobalCredentialsFilePath, getGlobalDir, getProjectCredentialsFilePath, readAndParseJsonFileSafe, } from '../utils/file.util.js';
|
|
5
3
|
let cachedCredential = null;
|
|
6
|
-
function readJsonSafe(filePath) {
|
|
7
|
-
try {
|
|
8
|
-
const contents = fs.readFileSync(filePath, 'utf8');
|
|
9
|
-
return JSON.parse(contents);
|
|
10
|
-
}
|
|
11
|
-
catch {
|
|
12
|
-
return null;
|
|
13
|
-
}
|
|
14
|
-
}
|
|
15
4
|
export function resolveCredential() {
|
|
16
5
|
if (process.env.DBDIAGRAM_TOKEN) {
|
|
17
6
|
cachedCredential = { token: process.env.DBDIAGRAM_TOKEN };
|
|
18
7
|
return cachedCredential;
|
|
19
8
|
}
|
|
20
|
-
const
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
cachedCredential = { token: projectCred.token };
|
|
9
|
+
const projectCredential = readAndParseJsonFileSafe(getProjectCredentialsFilePath());
|
|
10
|
+
if (projectCredential && typeof projectCredential.token === 'string') {
|
|
11
|
+
cachedCredential = { token: projectCredential.token };
|
|
24
12
|
return cachedCredential;
|
|
25
13
|
}
|
|
26
|
-
const
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
cachedCredential = { token: userCred.token };
|
|
14
|
+
const globalCredential = readAndParseJsonFileSafe(getGlobalCredentialsFilePath());
|
|
15
|
+
if (globalCredential && typeof globalCredential.token === 'string') {
|
|
16
|
+
cachedCredential = { token: globalCredential.token };
|
|
30
17
|
return cachedCredential;
|
|
31
18
|
}
|
|
32
19
|
cachedCredential = null;
|
|
33
20
|
return null;
|
|
34
21
|
}
|
|
35
22
|
export function storeCredential(token) {
|
|
36
|
-
const dir =
|
|
37
|
-
const filePath =
|
|
23
|
+
const dir = getGlobalDir();
|
|
24
|
+
const filePath = getGlobalCredentialsFilePath();
|
|
38
25
|
fs.mkdirSync(dir, { recursive: true });
|
|
39
26
|
fs.writeFileSync(filePath, JSON.stringify({ token }, null, 2), { mode: 0o600 });
|
|
40
27
|
cachedCredential = { token };
|
|
@@ -46,7 +33,7 @@ export function getAccessToken() {
|
|
|
46
33
|
return cachedCredential?.token ?? null;
|
|
47
34
|
}
|
|
48
35
|
export function clearCredential() {
|
|
49
|
-
const filePath =
|
|
36
|
+
const filePath = getGlobalCredentialsFilePath();
|
|
50
37
|
fs.rmSync(filePath, { force: true });
|
|
51
38
|
cachedCredential = null;
|
|
52
39
|
}
|
|
@@ -1,12 +1,8 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
|
-
import
|
|
3
|
-
import { DOT_DBDIAGRAM_DIR, SETTINGS_FILE_NAME } from '../constants/dot-dbdiagram-dir.constant.js';
|
|
2
|
+
import { getProjectDir, getSettingsFilePath, readAndParseJsonFileSafe } from '../utils/file.util.js';
|
|
4
3
|
let cachedSettings = null;
|
|
5
|
-
function settingsPath() {
|
|
6
|
-
return path.join(process.cwd(), DOT_DBDIAGRAM_DIR, SETTINGS_FILE_NAME);
|
|
7
|
-
}
|
|
8
4
|
export function settingsExists() {
|
|
9
|
-
return fs.existsSync(
|
|
5
|
+
return fs.existsSync(getSettingsFilePath());
|
|
10
6
|
}
|
|
11
7
|
export function defaultSettings() {
|
|
12
8
|
return {
|
|
@@ -15,30 +11,17 @@ export function defaultSettings() {
|
|
|
15
11
|
document: { source: 'file', workspaceUrlName: '', projectUrlName: '' },
|
|
16
12
|
};
|
|
17
13
|
}
|
|
18
|
-
export
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
return null;
|
|
25
|
-
}
|
|
26
|
-
}
|
|
27
|
-
export function writeSettings(data) {
|
|
28
|
-
const dir = path.join(process.cwd(), DOT_DBDIAGRAM_DIR);
|
|
29
|
-
fs.mkdirSync(dir, { recursive: true });
|
|
30
|
-
fs.writeFileSync(settingsPath(), JSON.stringify(data, null, 2), 'utf8');
|
|
14
|
+
export const readSettings = () => {
|
|
15
|
+
return readAndParseJsonFileSafe(getSettingsFilePath());
|
|
16
|
+
};
|
|
17
|
+
export const writeSettings = (data) => {
|
|
18
|
+
fs.mkdirSync(getProjectDir(), { recursive: true });
|
|
19
|
+
fs.writeFileSync(getSettingsFilePath(), JSON.stringify(data, null, 2), 'utf8');
|
|
31
20
|
cachedSettings = data;
|
|
32
|
-
}
|
|
33
|
-
export
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
}
|
|
38
|
-
catch {
|
|
39
|
-
cachedSettings = null;
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
export function getSettings() {
|
|
21
|
+
};
|
|
22
|
+
export const resolveSettings = () => {
|
|
23
|
+
cachedSettings = readAndParseJsonFileSafe(getSettingsFilePath());
|
|
24
|
+
};
|
|
25
|
+
export const getSettings = () => {
|
|
43
26
|
return cachedSettings;
|
|
44
|
-
}
|
|
27
|
+
};
|
package/dist/config.js
CHANGED
|
@@ -12,3 +12,7 @@ export const DBDIAGRAM_CONFIGS = {
|
|
|
12
12
|
export const DBDOCS_CONFIGS = {
|
|
13
13
|
baseUrl: process.env.DBDOCS_BASE_URL || 'https://dbdocs.io',
|
|
14
14
|
};
|
|
15
|
+
export const TELEMETRY_CONFIGS = {
|
|
16
|
+
collectorUrl: process.env.TRACKING_COLLECTOR_URL || 'https://snowplow.holistics.io',
|
|
17
|
+
appId: process.env.TRACKING_APP_ID || 'dbdiagram-cli',
|
|
18
|
+
};
|
|
@@ -1,6 +1,13 @@
|
|
|
1
1
|
import { resolveCredential } from '../config/credential-manager.js';
|
|
2
2
|
import { resolveSettings } from '../config/settings-manager.js';
|
|
3
|
-
|
|
3
|
+
import { flushTelemetry, initTelemetry, trackEvent } from '../services/telemetry.service.js';
|
|
4
|
+
import { resolveCommandName } from '../utils/command-name.util.js';
|
|
5
|
+
export function globalPreAction(_thisCommand, actionCommand) {
|
|
4
6
|
resolveCredential();
|
|
5
7
|
resolveSettings();
|
|
8
|
+
initTelemetry();
|
|
9
|
+
trackEvent({ name: 'cli_command_run', properties: { command: resolveCommandName(actionCommand) } });
|
|
10
|
+
}
|
|
11
|
+
export function globalPostAction() {
|
|
12
|
+
flushTelemetry();
|
|
6
13
|
}
|
package/dist/index.js
CHANGED
|
@@ -1,12 +1,32 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import 'dotenv/config';
|
|
3
|
+
import { CommanderError } from 'commander';
|
|
3
4
|
import { load } from './commands/command.loader.js';
|
|
4
5
|
import { createProgram } from './program.js';
|
|
5
|
-
import { globalPreAction } from './hooks/global.hook.js';
|
|
6
|
+
import { globalPostAction, globalPreAction } from './hooks/global.hook.js';
|
|
7
|
+
import { flushTelemetry, initTelemetry, trackEvent } from './services/telemetry.service.js';
|
|
8
|
+
const getAttemptedCommand = () => {
|
|
9
|
+
const operands = process.argv.slice(2).filter((arg) => !arg.startsWith('-'));
|
|
10
|
+
return operands.length ? operands.join(' ') : 'unknown';
|
|
11
|
+
};
|
|
6
12
|
const bootstrap = async () => {
|
|
7
13
|
const program = createProgram();
|
|
8
14
|
load(program);
|
|
9
15
|
program.hook('preAction', globalPreAction);
|
|
10
|
-
|
|
16
|
+
program.hook('postAction', globalPostAction);
|
|
17
|
+
try {
|
|
18
|
+
await program.parseAsync(process.argv);
|
|
19
|
+
}
|
|
20
|
+
catch (error) {
|
|
21
|
+
if (!(error instanceof CommanderError))
|
|
22
|
+
throw error;
|
|
23
|
+
if (error.code === 'commander.unknownCommand' ||
|
|
24
|
+
error.code === 'commander.excessArguments') {
|
|
25
|
+
initTelemetry();
|
|
26
|
+
trackEvent({ name: 'cli_command_run', properties: { command: getAttemptedCommand() } });
|
|
27
|
+
flushTelemetry();
|
|
28
|
+
}
|
|
29
|
+
process.exitCode = error.exitCode;
|
|
30
|
+
}
|
|
11
31
|
};
|
|
12
32
|
bootstrap();
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { buildSelfDescribingEvent, newTracker } from '@snowplow/node-tracker';
|
|
2
|
+
import { TELEMETRY_CONFIGS } from '../../config.js';
|
|
3
|
+
export const constructSnowplowSchemaUrl = (name, version = '1-0-0') => `iglu:io.dbdiagram/${name}/jsonschema/${version}`;
|
|
4
|
+
let tracker;
|
|
5
|
+
export const getTracker = () => {
|
|
6
|
+
if (tracker)
|
|
7
|
+
return tracker;
|
|
8
|
+
tracker = newTracker({
|
|
9
|
+
namespace: 'cli',
|
|
10
|
+
appId: TELEMETRY_CONFIGS.appId,
|
|
11
|
+
encodeBase64: true,
|
|
12
|
+
}, {
|
|
13
|
+
endpoint: TELEMETRY_CONFIGS.collectorUrl,
|
|
14
|
+
eventMethod: 'post',
|
|
15
|
+
bufferSize: 1,
|
|
16
|
+
retryFailedRequests: false,
|
|
17
|
+
onRequestFailure: () => { },
|
|
18
|
+
onRequestSuccess() { },
|
|
19
|
+
});
|
|
20
|
+
return tracker;
|
|
21
|
+
};
|
|
22
|
+
export const toEventData = (event) => {
|
|
23
|
+
const { name, properties } = event;
|
|
24
|
+
switch (name) {
|
|
25
|
+
case 'cli_command_run':
|
|
26
|
+
return { command: properties.command };
|
|
27
|
+
case 'cli_diagram_push': {
|
|
28
|
+
const { action, diagramId, workspaceId } = properties;
|
|
29
|
+
return {
|
|
30
|
+
action,
|
|
31
|
+
diagram_id: diagramId,
|
|
32
|
+
workspace_id: workspaceId,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
case 'cli_document_build': {
|
|
36
|
+
const { workspaceUrlName, projectUrlName } = properties;
|
|
37
|
+
return {
|
|
38
|
+
workspace_url_name: workspaceUrlName,
|
|
39
|
+
project_url_name: projectUrlName,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
export const toClientContextEntity = (client) => ({
|
|
45
|
+
schema: constructSnowplowSchemaUrl('cli_client_context'),
|
|
46
|
+
data: {
|
|
47
|
+
device_id: client.deviceId,
|
|
48
|
+
invocation_id: client.invocationId,
|
|
49
|
+
cli_version: client.cliVersion,
|
|
50
|
+
node_version: client.nodeVersion,
|
|
51
|
+
os: client.os,
|
|
52
|
+
arch: client.arch,
|
|
53
|
+
actor: client.actor,
|
|
54
|
+
...(client.agentName ? { agent_name: client.agentName } : {}),
|
|
55
|
+
environment: client.environment,
|
|
56
|
+
},
|
|
57
|
+
});
|
|
58
|
+
export const toUserContextEntity = (userId) => ({
|
|
59
|
+
schema: constructSnowplowSchemaUrl('cli_user_context'),
|
|
60
|
+
data: { user_id: userId },
|
|
61
|
+
});
|
|
62
|
+
export const send = (events, client) => {
|
|
63
|
+
const tracker = getTracker();
|
|
64
|
+
const clientContextEntity = toClientContextEntity(client);
|
|
65
|
+
for (const event of events) {
|
|
66
|
+
const contexts = [clientContextEntity];
|
|
67
|
+
if ('userId' in event) {
|
|
68
|
+
contexts.push(toUserContextEntity(event.userId));
|
|
69
|
+
}
|
|
70
|
+
tracker.track(buildSelfDescribingEvent({
|
|
71
|
+
event: { schema: constructSnowplowSchemaUrl(event.name), data: toEventData(event) },
|
|
72
|
+
}), contexts, { type: 'ttm', value: event.timestamp });
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
export const telemetryIntegration = {
|
|
76
|
+
send,
|
|
77
|
+
};
|
package/dist/program.js
CHANGED
|
@@ -9,8 +9,9 @@ export function createProgram() {
|
|
|
9
9
|
.helpOption('-h, --help', 'Show help information')
|
|
10
10
|
.enablePositionalOptions(true)
|
|
11
11
|
.action(() => {
|
|
12
|
-
program.
|
|
12
|
+
program.outputHelp();
|
|
13
13
|
})
|
|
14
14
|
.showHelpAfterError(true);
|
|
15
|
+
program.exitOverride();
|
|
15
16
|
return program;
|
|
16
17
|
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import crypto from 'node:crypto';
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import packageJson from '../../package.json' with { type: 'json' };
|
|
4
|
+
import { telemetryIntegration } from '../integrations/telemetry/telemetry.integration.js';
|
|
5
|
+
import { detectAgent } from '../utils/agent-detector.util.js';
|
|
6
|
+
import { detectExecuteEnvironment } from '../utils/environment-detector.util.js';
|
|
7
|
+
import { getGlobalDir, getTelemetryFilePath, readAndParseJsonFileSafe } from '../utils/file.util.js';
|
|
8
|
+
const events = [];
|
|
9
|
+
let deviceId;
|
|
10
|
+
let invocationId;
|
|
11
|
+
export const getOrCreateDeviceId = () => {
|
|
12
|
+
const telemetryFilePath = getTelemetryFilePath();
|
|
13
|
+
const telemetrySettings = readAndParseJsonFileSafe(telemetryFilePath);
|
|
14
|
+
if (telemetrySettings && telemetrySettings.deviceId) {
|
|
15
|
+
return telemetrySettings.deviceId;
|
|
16
|
+
}
|
|
17
|
+
const deviceId = crypto.randomUUID();
|
|
18
|
+
fs.mkdirSync(getGlobalDir(), { recursive: true });
|
|
19
|
+
fs.writeFileSync(telemetryFilePath, JSON.stringify({ deviceId }, null, 2));
|
|
20
|
+
return deviceId;
|
|
21
|
+
};
|
|
22
|
+
export const constructCliClientContext = () => {
|
|
23
|
+
const { actor, agentName } = detectAgent();
|
|
24
|
+
return {
|
|
25
|
+
deviceId: deviceId ?? '',
|
|
26
|
+
invocationId: invocationId ?? '',
|
|
27
|
+
cliVersion: packageJson.version,
|
|
28
|
+
nodeVersion: process.versions.node,
|
|
29
|
+
os: process.platform,
|
|
30
|
+
arch: process.arch,
|
|
31
|
+
actor,
|
|
32
|
+
...(agentName ? { agentName } : {}),
|
|
33
|
+
environment: detectExecuteEnvironment(),
|
|
34
|
+
};
|
|
35
|
+
};
|
|
36
|
+
export const initTelemetry = () => {
|
|
37
|
+
try {
|
|
38
|
+
deviceId = getOrCreateDeviceId();
|
|
39
|
+
invocationId = crypto.randomUUID();
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
export const trackEvent = (event) => {
|
|
45
|
+
try {
|
|
46
|
+
events.push({ ...event, timestamp: Date.now() });
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
export const flushTelemetry = () => {
|
|
52
|
+
try {
|
|
53
|
+
if (!events.length)
|
|
54
|
+
return;
|
|
55
|
+
telemetryIntegration.send(events, constructCliClientContext());
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
}
|
|
59
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
const AGENT_ENV_VAR_TO_NAME = {
|
|
2
|
+
CURSOR_TRACE_ID: 'cursor',
|
|
3
|
+
CURSOR_AGENT: 'cursor-cli',
|
|
4
|
+
CURSOR_EXTENSION_HOST_ROLE: 'cursor-cli',
|
|
5
|
+
GEMINI_CLI: 'gemini',
|
|
6
|
+
CODEX_SANDBOX: 'codex',
|
|
7
|
+
CODEX_CI: 'codex',
|
|
8
|
+
CODEX_THREAD_ID: 'codex',
|
|
9
|
+
CLAUDECODE: 'claude',
|
|
10
|
+
CLAUDE_CODE: 'claude',
|
|
11
|
+
REPL_ID: 'replit',
|
|
12
|
+
COPILOT_MODEL: 'github-copilot',
|
|
13
|
+
COPILOT_ALLOW_ALL: 'github-copilot',
|
|
14
|
+
COPILOT_GITHUB_TOKEN: 'github-copilot',
|
|
15
|
+
ANTIGRAVITY_AGENT: 'antigravity',
|
|
16
|
+
AUGMENT_AGENT: 'augment-cli',
|
|
17
|
+
OPENCODE_CLIENT: 'opencode',
|
|
18
|
+
OPENCODE: 'opencode',
|
|
19
|
+
};
|
|
20
|
+
export const detectAgent = () => {
|
|
21
|
+
for (const [envVar, agentName] of Object.entries(AGENT_ENV_VAR_TO_NAME)) {
|
|
22
|
+
if (process.env[envVar]) {
|
|
23
|
+
return { actor: 'ai_agent', agentName };
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
return { actor: 'human' };
|
|
27
|
+
};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export function resolveCommandName(actionCommand) {
|
|
2
|
+
const nameSegments = [];
|
|
3
|
+
let currentCommand = actionCommand;
|
|
4
|
+
while (currentCommand && currentCommand.parent) {
|
|
5
|
+
nameSegments.unshift(currentCommand.name());
|
|
6
|
+
currentCommand = currentCommand.parent;
|
|
7
|
+
}
|
|
8
|
+
if (nameSegments.length === 0) {
|
|
9
|
+
return actionCommand.name();
|
|
10
|
+
}
|
|
11
|
+
return nameSegments.join(' ');
|
|
12
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { homedir } from 'node:os';
|
|
4
|
+
import { DOT_DBDIAGRAM_DIR, CREDENTIALS_FILE_NAME, SETTINGS_FILE_NAME, TELEMETRY_FILE_NAME, } from '../constants/dot-dbdiagram-dir.constant.js';
|
|
5
|
+
export const readAndParseJsonFileSafe = (filePath) => {
|
|
6
|
+
try {
|
|
7
|
+
const contents = readFileSync(filePath, 'utf8');
|
|
8
|
+
return JSON.parse(contents);
|
|
9
|
+
}
|
|
10
|
+
catch {
|
|
11
|
+
return null;
|
|
12
|
+
}
|
|
13
|
+
};
|
|
14
|
+
export const getProjectDir = () => {
|
|
15
|
+
return join(process.cwd(), DOT_DBDIAGRAM_DIR);
|
|
16
|
+
};
|
|
17
|
+
export const getGlobalDir = () => {
|
|
18
|
+
return join(homedir(), DOT_DBDIAGRAM_DIR);
|
|
19
|
+
};
|
|
20
|
+
export const getSettingsFilePath = () => {
|
|
21
|
+
return join(getProjectDir(), SETTINGS_FILE_NAME);
|
|
22
|
+
};
|
|
23
|
+
export const getProjectCredentialsFilePath = () => {
|
|
24
|
+
return join(getProjectDir(), CREDENTIALS_FILE_NAME);
|
|
25
|
+
};
|
|
26
|
+
export const getGlobalCredentialsFilePath = () => {
|
|
27
|
+
return join(getGlobalDir(), CREDENTIALS_FILE_NAME);
|
|
28
|
+
};
|
|
29
|
+
export const getTelemetryFilePath = () => {
|
|
30
|
+
return join(getGlobalDir(), TELEMETRY_FILE_NAME);
|
|
31
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dbdiagram",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"main": "dist/index.js",
|
|
5
5
|
"bin": {
|
|
6
6
|
"dbdiagram": "dist/index.js"
|
|
@@ -31,7 +31,9 @@
|
|
|
31
31
|
"dependencies": {
|
|
32
32
|
"@clack/prompts": "^1.4.0",
|
|
33
33
|
"@dbml/core": "8.2.5",
|
|
34
|
+
"@snowplow/node-tracker": "^4.9.0",
|
|
34
35
|
"axios": "^1.16.1",
|
|
36
|
+
"ci-info": "^4.4.0",
|
|
35
37
|
"cli-table3": "^0.6.5",
|
|
36
38
|
"commander": "^15.0.0",
|
|
37
39
|
"dotenv": "^16.4.0",
|