dbdiagram 0.1.2 → 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.
Files changed (42) hide show
  1. package/README.md +13 -0
  2. package/dist/actions/auth/auth-login.action.js +8 -19
  3. package/dist/actions/auth/auth-logout.action.js +11 -3
  4. package/dist/actions/auth/auth-status.action.js +7 -13
  5. package/dist/actions/build/build-document.action.js +29 -45
  6. package/dist/actions/delete.action.js +8 -15
  7. package/dist/actions/list/list-document.action.js +13 -19
  8. package/dist/actions/list/list.action.js +12 -19
  9. package/dist/actions/pull.action.js +8 -17
  10. package/dist/actions/push.action.js +22 -24
  11. package/dist/actions/tokens/token-delete.action.js +8 -14
  12. package/dist/actions/tokens/token-generate.action.js +7 -12
  13. package/dist/actions/tokens/token-list.action.js +10 -17
  14. package/dist/commands/auth/auth-logout.command.js +1 -0
  15. package/dist/commands/auth/auth.command.js +1 -1
  16. package/dist/commands/build/build.command.js +1 -1
  17. package/dist/commands/tokens/tokens.command.js +1 -1
  18. package/dist/config/credential-manager.js +10 -23
  19. package/dist/config/settings-manager.js +14 -31
  20. package/dist/config.js +4 -0
  21. package/dist/constants/cli-error.constant.js +81 -0
  22. package/dist/constants/dot-dbdiagram-dir.constant.js +1 -0
  23. package/dist/errors/cli.error.js +9 -0
  24. package/dist/errors/portal-api.error.js +1 -0
  25. package/dist/hooks/global.hook.js +8 -1
  26. package/dist/index.js +22 -2
  27. package/dist/integrations/telemetry/telemetry.integration.js +77 -0
  28. package/dist/program.js +2 -1
  29. package/dist/services/telemetry.service.js +59 -0
  30. package/dist/types/telemetry.type.js +1 -0
  31. package/dist/utils/agent-detector.util.js +27 -0
  32. package/dist/utils/cli-error.util.js +46 -0
  33. package/dist/utils/command-name.util.js +12 -0
  34. package/dist/utils/date.util.js +4 -0
  35. package/dist/utils/environment-detector.util.js +6 -0
  36. package/dist/utils/file.util.js +31 -0
  37. package/dist/utils/output.util.js +35 -4
  38. package/package.json +4 -2
  39. package/dist/constants/auth-message.constant.js +0 -1
  40. package/dist/errors/portal-error-codes.js +0 -23
  41. package/dist/utils/portal-error.util.js +0 -18
  42. package/dist/utils/table.util.js +0 -10
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
@@ -1,19 +1,13 @@
1
1
  import path from 'path';
2
- import { spinner, log } from '@clack/prompts';
2
+ import { spinner } from '@clack/prompts';
3
3
  import open from 'open';
4
4
  import { storeCredential } from '../../config/credential-manager.js';
5
5
  import { PORTAL_LOGIN_CONFIGS, PORTAL_URLS } from '../../config.js';
6
- import { initAuthClient, PORTAL_AUTH_ERROR_CODES, PortalAuthenticationError, } from '../../libs/portal/index.js';
7
- const ERROR_MESSAGES = {
8
- [PORTAL_AUTH_ERROR_CODES.loginTimeout]: 'Login timed out. Please try again.',
9
- [PORTAL_AUTH_ERROR_CODES.loginCancelled]: 'Login cancelled.',
10
- [PORTAL_AUTH_ERROR_CODES.stateMismatch]: 'Authentication failed: security validation error.',
11
- [PORTAL_AUTH_ERROR_CODES.exchangeFailed]: 'Authentication failed: could not exchange token.',
12
- [PORTAL_AUTH_ERROR_CODES.invalidCallback]: 'Authentication failed: invalid callback received.',
13
- [PORTAL_AUTH_ERROR_CODES.loopbackServerError]: 'Authentication failed: could not start local server.',
14
- [PORTAL_AUTH_ERROR_CODES.unknownError]: 'Authentication failed: an unexpected error occurred.',
15
- };
6
+ import { initAuthClient } from '../../libs/portal/index.js';
7
+ import { toCliError } from '../../utils/cli-error.util.js';
8
+ import { displayError } from '../../utils/output.util.js';
16
9
  export async function loginAction() {
10
+ const s = spinner();
17
11
  try {
18
12
  const authClient = await initAuthClient({
19
13
  loginUrl: PORTAL_URLS.loginUrl,
@@ -25,7 +19,6 @@ export async function loginAction() {
25
19
  error: path.join(import.meta.dirname, '../../assets/callback-error.html'),
26
20
  },
27
21
  });
28
- const s = spinner();
29
22
  await open(authClient.getLoginUrl());
30
23
  s.start('Waiting for browser login...');
31
24
  const { accessToken } = await authClient.obtainTokens();
@@ -33,12 +26,8 @@ export async function loginAction() {
33
26
  s.stop('Login successful. Credentials saved.');
34
27
  }
35
28
  catch (error) {
36
- if (error instanceof PortalAuthenticationError) {
37
- const message = ERROR_MESSAGES[error.errorCode] || ERROR_MESSAGES[PORTAL_AUTH_ERROR_CODES.unknownError];
38
- log.error(message);
39
- process.exitCode = 1;
40
- return;
41
- }
42
- throw error;
29
+ s.stop();
30
+ displayError(toCliError(error), 'plain');
31
+ process.exitCode = 1;
43
32
  }
44
33
  }
@@ -1,13 +1,21 @@
1
1
  import { log } from '@clack/prompts';
2
2
  import { clearCredential } from '../../config/credential-manager.js';
3
- export async function logoutAction() {
3
+ import { CliError } from '../../errors/cli.error.js';
4
+ import { AUTH_LOGOUT_FAILED } from '../../constants/cli-error.constant.js';
5
+ import { displayError, getOutputFormat, printJson } from '../../utils/output.util.js';
6
+ export async function logoutAction(options = {}) {
4
7
  try {
5
8
  clearCredential();
6
- log.success('Logout successful.');
9
+ if (options.json) {
10
+ printJson({ success: true });
11
+ }
12
+ else {
13
+ log.success('Logout successful.');
14
+ }
7
15
  }
8
16
  catch (error) {
9
17
  const message = error instanceof Error ? error.message : String(error);
10
- log.error(`Failed to log out: ${message}`);
18
+ displayError(new CliError(AUTH_LOGOUT_FAILED, message), getOutputFormat(options.json));
11
19
  process.exitCode = 1;
12
20
  }
13
21
  }
@@ -1,15 +1,15 @@
1
- import { log, spinner } from '@clack/prompts';
1
+ import { spinner } from '@clack/prompts';
2
2
  import { getCredential } from '../../config/credential-manager.js';
3
- import { NOT_LOGGED_IN_MESSAGE } from '../../constants/auth-message.constant.js';
3
+ import { AUTH_NOT_LOGGED_IN } from '../../constants/cli-error.constant.js';
4
+ import { CliError } from '../../errors/cli.error.js';
4
5
  import { portalIntegration } from '../../integrations/portal/portal.integration.js';
5
- import { isPortalApiError, getPortalApiErrorMessage } from '../../utils/portal-error.util.js';
6
- import { printJson } from '../../utils/output.util.js';
6
+ import { getCliErrorMessageFromCode, toCliError } from '../../utils/cli-error.util.js';
7
+ import { displayError, getOutputFormat, printJson } from '../../utils/output.util.js';
7
8
  export async function statusAction(options = {}) {
8
9
  const credential = getCredential();
9
10
  if (!credential) {
10
- log.warn(NOT_LOGGED_IN_MESSAGE);
11
11
  process.exitCode = 1;
12
- return;
12
+ return displayError(new CliError(AUTH_NOT_LOGGED_IN, getCliErrorMessageFromCode(AUTH_NOT_LOGGED_IN)), getOutputFormat(options.json));
13
13
  }
14
14
  const s = spinner();
15
15
  s.start('Checking login status...');
@@ -26,13 +26,7 @@ export async function statusAction(options = {}) {
26
26
  }
27
27
  catch (error) {
28
28
  s.stop('Failed to check login status.');
29
- if (isPortalApiError(error)) {
30
- log.error(getPortalApiErrorMessage(error));
31
- }
32
- else {
33
- const message = error instanceof Error ? error.message : String(error);
34
- log.error(message);
35
- }
29
+ displayError(toCliError(error), getOutputFormat(options.json));
36
30
  process.exitCode = 1;
37
31
  }
38
32
  }
@@ -1,64 +1,56 @@
1
1
  import { readFile } from 'node:fs/promises';
2
2
  import { log, spinner } from '@clack/prompts';
3
+ import { DBDOCS_CONFIGS } from '../../config.js';
3
4
  import { getCredential } from '../../config/credential-manager.js';
4
- import { NOT_LOGGED_IN_MESSAGE } from '../../constants/auth-message.constant.js';
5
+ import { DOCUMENT_SOURCE_TYPE } from '../../constants/document.constant.js';
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
+ import { CliError } from '../../errors/cli.error.js';
8
+ import { portalIntegration } from '../../integrations/portal/portal.integration.js';
9
+ import { trackEvent } from '../../services/telemetry.service.js';
5
10
  import { prepareBuildDocumentData } from '../../services/dbml/dbml.service.js';
11
+ import { getCliErrorMessageFromCode, toCliError } from '../../utils/cli-error.util.js';
6
12
  import { formatDbmlError, getDbmlVersion } from '../../utils/dbml.util.js';
7
- import { portalIntegration } from '../../integrations/portal/portal.integration.js';
8
- import { DBDOCS_CONFIGS } from '../../config.js';
9
- import { isPortalApiError, getPortalApiErrorMessage } from '../../utils/portal-error.util.js';
10
- import { printJson } from '../../utils/output.util.js';
13
+ import { displayError, getOutputFormat, printJson } from '../../utils/output.util.js';
11
14
  import { validateProjectUrlName, validateVersionName, validateWorkspaceUrlName, } from '../../utils/validation.util.js';
12
- import { DOCUMENT_SOURCE_TYPE } from '../../constants/document.constant.js';
13
15
  const DOCUMENT_SOURCE_CLIENT_TYPE = 'dbdiagram-cli';
14
16
  export async function documentAction(options) {
15
17
  const credential = getCredential();
16
18
  if (!credential) {
17
- log.error(NOT_LOGGED_IN_MESSAGE);
18
19
  process.exitCode = 1;
19
- return;
20
+ return displayError(new CliError(AUTH_NOT_LOGGED_IN, getCliErrorMessageFromCode(AUTH_NOT_LOGGED_IN)), getOutputFormat(options.json));
20
21
  }
21
22
  if (options.versionName !== undefined) {
22
23
  const versionError = validateVersionName(options.versionName);
23
24
  if (versionError) {
24
- log.error(versionError);
25
25
  process.exitCode = 1;
26
- return;
26
+ return displayError(new CliError(VERSION_NAME_INVALID, versionError), getOutputFormat(options.json));
27
27
  }
28
28
  }
29
29
  if (!options.workspaceUrlName) {
30
- log.error('Workspace url name is required. Use --project <workspace>/<project> '
31
- + 'or configure workspaceUrlName in settings.json');
32
30
  process.exitCode = 1;
33
- return;
31
+ return displayError(new CliError(WORKSPACE_NAME_MISSING, getCliErrorMessageFromCode(WORKSPACE_NAME_MISSING)), getOutputFormat(options.json));
34
32
  }
35
33
  const workspaceError = validateWorkspaceUrlName(options.workspaceUrlName);
36
34
  if (workspaceError) {
37
- log.error(workspaceError);
38
35
  process.exitCode = 1;
39
- return;
36
+ return displayError(new CliError(WORKSPACE_NAME_INVALID, workspaceError), getOutputFormat(options.json));
40
37
  }
41
38
  if (!options.projectUrlOrName) {
42
- log.error('Project name/url name is required. Use --project <workspace>/<project> '
43
- + 'or configure projectUrlName in settings.json');
44
39
  process.exitCode = 1;
45
- return;
40
+ return displayError(new CliError(PROJECT_NAME_MISSING, getCliErrorMessageFromCode(PROJECT_NAME_MISSING)), getOutputFormat(options.json));
46
41
  }
47
42
  if (options.projectUrlOrName.includes('/')) {
48
- log.error('Invalid --project value. Expected <workspace>/<project> or <project>.');
49
43
  process.exitCode = 1;
50
- return;
44
+ return displayError(new CliError(PROJECT_NAME_INVALID, 'Invalid --project value. Expected <workspace>/<project> or <project>.'), getOutputFormat(options.json));
51
45
  }
52
46
  const projectError = validateProjectUrlName(options.projectUrlOrName);
53
47
  if (projectError) {
54
- log.error(projectError);
55
48
  process.exitCode = 1;
56
- return;
49
+ return displayError(new CliError(PROJECT_NAME_INVALID, projectError), getOutputFormat(options.json));
57
50
  }
58
51
  if (options.source === undefined) {
59
- log.error('One source must be specified: --from-file or --from-diagram');
60
52
  process.exitCode = 1;
61
- return;
53
+ return displayError(new CliError(SOURCE_MISSING, getCliErrorMessageFromCode(SOURCE_MISSING)), getOutputFormat(options.json));
62
54
  }
63
55
  let content;
64
56
  if (options.source.type === DOCUMENT_SOURCE_TYPE.FILE) {
@@ -67,9 +59,8 @@ export async function documentAction(options) {
67
59
  }
68
60
  catch (error) {
69
61
  const message = error instanceof Error ? error.message : String(error);
70
- log.error(`Failed to read file: ${message}`);
71
62
  process.exitCode = 1;
72
- return;
63
+ return displayError(new CliError(FILE_READ_ERROR, `Failed to read file: ${message}`), getOutputFormat(options.json));
73
64
  }
74
65
  }
75
66
  else {
@@ -78,15 +69,8 @@ export async function documentAction(options) {
78
69
  content = diagram.content;
79
70
  }
80
71
  catch (error) {
81
- if (isPortalApiError(error)) {
82
- log.error(getPortalApiErrorMessage(error));
83
- }
84
- else {
85
- const message = error instanceof Error ? error.message : String(error);
86
- log.error(`Failed to fetch diagram: ${message}`);
87
- }
88
72
  process.exitCode = 1;
89
- return;
73
+ return displayError(toCliError(error), getOutputFormat(options.json));
90
74
  }
91
75
  }
92
76
  if (!content || !content.trim()) {
@@ -99,13 +83,13 @@ export async function documentAction(options) {
99
83
  preparedDocumentData = await prepareBuildDocumentData(content);
100
84
  }
101
85
  catch (error) {
102
- log.error(`Failed to parse DBML: ${formatDbmlError(error)}`);
103
86
  process.exitCode = 1;
104
- return;
87
+ return displayError(new CliError(DBML_PARSE_ERROR, `Failed to parse DBML: ${formatDbmlError(error)}`), getOutputFormat(options.json));
105
88
  }
106
89
  const s = spinner();
107
90
  s.start('Building document on dbdocs...');
108
91
  try {
92
+ const currentUser = await portalIntegration.getCurrentUser();
109
93
  const { project } = await portalIntegration.createDocument({
110
94
  projectName: options.projectUrlOrName,
111
95
  projectDescription: preparedDocumentData.description,
@@ -119,10 +103,16 @@ export async function documentAction(options) {
119
103
  normalizedDatabase: preparedDocumentData.normalizedDatabase,
120
104
  dbmlVersion: getDbmlVersion(),
121
105
  });
122
- const url = `${DBDOCS_CONFIGS.baseUrl}/${project.workspaceUrl}/${project.urlName}`;
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}`;
123
113
  if (options.json) {
124
114
  s.stop();
125
- printJson({ projectUrlName: `${project.workspaceUrl}/${project.urlName}`, url });
115
+ printJson({ projectUrlName: `${workspaceUrlName}/${projectUrlName}`, url });
126
116
  }
127
117
  else {
128
118
  s.stop(`Document built successfully. Visit: ${url}`);
@@ -130,13 +120,7 @@ export async function documentAction(options) {
130
120
  }
131
121
  catch (error) {
132
122
  s.stop('Failed to build document.');
133
- if (isPortalApiError(error)) {
134
- log.error(getPortalApiErrorMessage(error));
135
- }
136
- else {
137
- const message = error instanceof Error ? error.message : String(error);
138
- log.error(message);
139
- }
123
+ displayError(toCliError(error), getOutputFormat(options.json));
140
124
  process.exitCode = 1;
141
125
  }
142
126
  }
@@ -1,21 +1,20 @@
1
1
  import { confirm, isCancel, log, spinner } from '@clack/prompts';
2
2
  import { getCredential } from '../config/credential-manager.js';
3
- import { NOT_LOGGED_IN_MESSAGE } from '../constants/auth-message.constant.js';
4
3
  import { getSettings, writeSettings } from '../config/settings-manager.js';
4
+ import { AUTH_NOT_LOGGED_IN, DIAGRAM_ID_MISSING } from '../constants/cli-error.constant.js';
5
+ import { CliError } from '../errors/cli.error.js';
5
6
  import { portalIntegration } from '../integrations/portal/portal.integration.js';
6
- import { isPortalApiError, getPortalApiErrorMessage } from '../utils/portal-error.util.js';
7
- import { printJson } from '../utils/output.util.js';
7
+ import { getCliErrorMessageFromCode, toCliError } from '../utils/cli-error.util.js';
8
+ import { displayError, getOutputFormat, printJson } from '../utils/output.util.js';
8
9
  export async function deleteAction(options) {
9
10
  const credential = getCredential();
10
11
  if (!credential) {
11
- log.error(NOT_LOGGED_IN_MESSAGE);
12
12
  process.exitCode = 1;
13
- return;
13
+ return displayError(new CliError(AUTH_NOT_LOGGED_IN, getCliErrorMessageFromCode(AUTH_NOT_LOGGED_IN)), getOutputFormat(options.json));
14
14
  }
15
15
  if (!options.diagramId) {
16
- log.error('Missing required flag: --diagram-id');
17
16
  process.exitCode = 1;
18
- return;
17
+ return displayError(new CliError(DIAGRAM_ID_MISSING, getCliErrorMessageFromCode(DIAGRAM_ID_MISSING)), getOutputFormat(options.json));
19
18
  }
20
19
  if (!options.force) {
21
20
  const shouldDelete = await confirm({
@@ -42,7 +41,7 @@ export async function deleteAction(options) {
42
41
  printJson({
43
42
  diagramId: options.diagramId,
44
43
  deleted: true,
45
- ...(isRemoveInSettings ? {} : { diagramIdSettingCleared: true }),
44
+ ...(isRemoveInSettings ? { diagramIdSettingCleared: true } : {}),
46
45
  });
47
46
  }
48
47
  else {
@@ -55,13 +54,7 @@ export async function deleteAction(options) {
55
54
  catch (error) {
56
55
  s.error('Failed to delete diagram.');
57
56
  s.stop();
58
- if (isPortalApiError(error)) {
59
- log.error(getPortalApiErrorMessage(error));
60
- }
61
- else {
62
- const message = error instanceof Error ? error.message : String(error);
63
- log.error(message);
64
- }
57
+ displayError(toCliError(error), getOutputFormat(options.json));
65
58
  process.exitCode = 1;
66
59
  }
67
60
  }
@@ -1,12 +1,13 @@
1
1
  import { log, spinner } from '@clack/prompts';
2
2
  import { DBDOCS_CONFIGS } from '../../config.js';
3
3
  import { getCredential } from '../../config/credential-manager.js';
4
- import { NOT_LOGGED_IN_MESSAGE } from '../../constants/auth-message.constant.js';
4
+ import { CliError } from '../../errors/cli.error.js';
5
+ import { getCliErrorMessageFromCode, toCliError } from '../../utils/cli-error.util.js';
6
+ import { AUTH_NOT_LOGGED_IN } from '../../constants/cli-error.constant.js';
5
7
  import { portalIntegration } from '../../integrations/portal/portal.integration.js';
8
+ import { formatShortDateTime } from '../../utils/date.util.js';
6
9
  import { logConsole } from '../../utils/logger.util.js';
7
- import { printJson, printTable } from '../../utils/output.util.js';
8
- import { isPortalApiError, getPortalApiErrorMessage } from '../../utils/portal-error.util.js';
9
- import { computeColWidths } from '../../utils/table.util.js';
10
+ import { displayError, getOutputFormat, printJson, printTable } from '../../utils/output.util.js';
10
11
  function mapRole(role) {
11
12
  switch (role) {
12
13
  case 'admin':
@@ -34,9 +35,8 @@ function mapAccessControl(ac) {
34
35
  export async function listDocumentAction(opts) {
35
36
  const credential = getCredential();
36
37
  if (!credential) {
37
- log.error(NOT_LOGGED_IN_MESSAGE);
38
38
  process.exitCode = 1;
39
- return;
39
+ return displayError(new CliError(AUTH_NOT_LOGGED_IN, getCliErrorMessageFromCode(AUTH_NOT_LOGGED_IN)), getOutputFormat(opts.json));
40
40
  }
41
41
  const s = spinner();
42
42
  s.start('Fetching documents...');
@@ -87,30 +87,29 @@ export async function listDocumentAction(opts) {
87
87
  return {
88
88
  workspace: w.urlName,
89
89
  name: p.name,
90
+ url: `${DBDOCS_CONFIGS.baseUrl}/${w.urlName}/${p.urlName}`,
90
91
  owner: ownerLabel,
91
92
  permission: mapRole(p.role),
92
93
  accessControl: mapAccessControl(p.accessControl),
93
- url: `${DBDOCS_CONFIGS.baseUrl}/${w.urlName}/${p.urlName}`,
94
- updatedAt: p.updatedAt,
94
+ updatedAt: formatShortDateTime(p.updatedAt),
95
95
  };
96
96
  }));
97
97
  printJson(allMappedData);
98
98
  }
99
99
  else {
100
100
  for (const w of workspacesWithProjects) {
101
- logConsole(`\n${w.urlName}`);
101
+ logConsole(`\nWorkspace: ${w.urlName}`);
102
102
  printTable(w.projects, {
103
- head: ['Name', 'Owner', 'Permission', 'Access Control', 'Url', 'Last updated'],
104
- colWidths: computeColWidths([0.14, 0.22, 0.12, 0.14, 0.24, 0.14]),
103
+ head: ['Name', 'Url', 'Owner', 'Permission', 'Access Control', 'Last updated'],
105
104
  rowMapper: (p) => {
106
105
  const ownerLabel = p.owner.email === currentUser.email ? `${p.owner.email} (you)` : p.owner.email;
107
106
  return [
108
107
  p.name,
108
+ `${DBDOCS_CONFIGS.baseUrl}/${w.urlName}/${p.urlName}`,
109
109
  ownerLabel,
110
110
  mapRole(p.role),
111
111
  mapAccessControl(p.accessControl),
112
- `${DBDOCS_CONFIGS.baseUrl}/${w.urlName}/${p.urlName}`,
113
- new Date(p.updatedAt).toLocaleString(),
112
+ formatShortDateTime(p.updatedAt),
114
113
  ];
115
114
  },
116
115
  });
@@ -119,12 +118,7 @@ export async function listDocumentAction(opts) {
119
118
  }
120
119
  catch (error) {
121
120
  s.stop('Failed to fetch documents.');
122
- if (isPortalApiError(error)) {
123
- log.error(getPortalApiErrorMessage(error));
124
- }
125
- else {
126
- log.error(error instanceof Error ? error.message : String(error));
127
- }
121
+ displayError(toCliError(error), getOutputFormat(opts.json));
128
122
  process.exitCode = 1;
129
123
  }
130
124
  }
@@ -1,11 +1,12 @@
1
- import { log, spinner } from '@clack/prompts';
1
+ import { spinner } from '@clack/prompts';
2
2
  import { DBDIAGRAM_CONFIGS } from '../../config.js';
3
3
  import { getCredential } from '../../config/credential-manager.js';
4
- import { NOT_LOGGED_IN_MESSAGE } from '../../constants/auth-message.constant.js';
4
+ import { CliError } from '../../errors/cli.error.js';
5
+ import { getCliErrorMessageFromCode, toCliError } from '../../utils/cli-error.util.js';
6
+ import { AUTH_NOT_LOGGED_IN } from '../../constants/cli-error.constant.js';
5
7
  import { portalIntegration } from '../../integrations/portal/portal.integration.js';
6
- import { isPortalApiError, getPortalApiErrorMessage } from '../../utils/portal-error.util.js';
7
- import { computeColWidths } from '../../utils/table.util.js';
8
- import { printJson, printTable } from '../../utils/output.util.js';
8
+ import { formatShortDateTime } from '../../utils/date.util.js';
9
+ import { displayError, getOutputFormat, printJson, printTable } from '../../utils/output.util.js';
9
10
  function mapStatus(status) {
10
11
  switch (status) {
11
12
  case 'public':
@@ -24,9 +25,8 @@ function ownerLabel(owner, currentUserEmail) {
24
25
  export async function listAction(opts) {
25
26
  const credential = getCredential();
26
27
  if (!credential) {
27
- log.error(NOT_LOGGED_IN_MESSAGE);
28
28
  process.exitCode = 1;
29
- return;
29
+ return displayError(new CliError(AUTH_NOT_LOGGED_IN, getCliErrorMessageFromCode(AUTH_NOT_LOGGED_IN)), getOutputFormat(opts.json));
30
30
  }
31
31
  const s = spinner();
32
32
  s.start('Fetching diagrams...');
@@ -52,7 +52,7 @@ export async function listAction(opts) {
52
52
  owner: ownerLabel(d.owner, currentUser.email),
53
53
  status: mapStatus(d.status),
54
54
  url: `${DBDIAGRAM_CONFIGS.baseUrl}/d/${d.id}`,
55
- updatedAt: new Date(d.updatedAt).toLocaleString(),
55
+ updatedAt: formatShortDateTime(d.updatedAt),
56
56
  }));
57
57
  if (opts.json) {
58
58
  printJson(mappedData);
@@ -60,7 +60,6 @@ export async function listAction(opts) {
60
60
  else {
61
61
  printTable(mappedData, {
62
62
  head: ['Name', 'ID', 'Owner', 'Status', 'Url', 'Last updated'],
63
- colWidths: computeColWidths([0.18, 0.22, 0.14, 0.09, 0.26, 0.11]),
64
63
  rowMapper: (d) => [d.name, d.id, d.owner, d.status, d.url, d.updatedAt],
65
64
  });
66
65
  }
@@ -76,28 +75,22 @@ export async function listAction(opts) {
76
75
  name: d.name,
77
76
  id: d.id,
78
77
  url: `${DBDIAGRAM_CONFIGS.baseUrl}/d/${d.id}`,
79
- updatedAt: new Date(d.updatedAt).toLocaleString(),
78
+ updatedAt: formatShortDateTime(d.updatedAt),
80
79
  }));
81
80
  if (opts.json) {
82
81
  printJson(mappedData);
83
82
  }
84
83
  else {
85
84
  printTable(mappedData, {
86
- head: ['Name', 'ID', 'Url', 'Last updated'],
87
- colWidths: computeColWidths([0.25, 0.18, 0.35, 0.22]),
88
- rowMapper: (d) => [d.name, d.id, d.url, d.updatedAt],
85
+ head: ['ID', 'Url', 'Name', 'Last updated'],
86
+ rowMapper: (d) => [d.id, d.url, d.name, d.updatedAt],
89
87
  });
90
88
  }
91
89
  }
92
90
  }
93
91
  catch (error) {
94
92
  s.stop('Failed to fetch diagrams.');
95
- if (isPortalApiError(error)) {
96
- log.error(getPortalApiErrorMessage(error));
97
- }
98
- else {
99
- log.error(error instanceof Error ? error.message : String(error));
100
- }
93
+ displayError(toCliError(error), getOutputFormat(opts.json));
101
94
  process.exitCode = 1;
102
95
  }
103
96
  }
@@ -1,22 +1,23 @@
1
- import { log, spinner } from '@clack/prompts';
2
- import { NOT_LOGGED_IN_MESSAGE } from '../constants/auth-message.constant.js';
1
+ import { spinner } from '@clack/prompts';
3
2
  import { getCredential } from '../config/credential-manager.js';
4
3
  import { writeFileWithDir } from '../services/file.service.js';
5
4
  import { portalIntegration } from '../integrations/portal/portal.integration.js';
6
5
  import { deriveDiagramVizPathFromDBML, readDiagramVizFile, writeDiagramVizFile, } from '../services/viz/diagram-viz-file.service.js';
7
- import { getVizReadErrorMessage } from '../utils/diagram-viz-error.util.js';
8
6
  import { fromServerFormat } from '../services/viz/diagram-viz-converter.service.js';
9
7
  import { logConsole } from '../utils/logger.util.js';
10
- import { getPortalApiErrorMessage, isPortalApiError } from '../utils/portal-error.util.js';
8
+ import { CliError } from '../errors/cli.error.js';
9
+ import { AUTH_NOT_LOGGED_IN, DIAGRAM_ID_MISSING } from '../constants/cli-error.constant.js';
10
+ import { getCliErrorMessageFromCode, toCliError } from '../utils/cli-error.util.js';
11
+ import { displayError } from '../utils/output.util.js';
11
12
  export async function pullAction(options) {
12
13
  const credential = getCredential();
13
14
  if (!credential) {
14
- log.error(NOT_LOGGED_IN_MESSAGE);
15
+ displayError(new CliError(AUTH_NOT_LOGGED_IN, getCliErrorMessageFromCode(AUTH_NOT_LOGGED_IN)), 'plain');
15
16
  process.exitCode = 1;
16
17
  return;
17
18
  }
18
19
  if (!options.diagramId) {
19
- log.error('Missing required flag: --diagram-id');
20
+ displayError(new CliError(DIAGRAM_ID_MISSING, getCliErrorMessageFromCode(DIAGRAM_ID_MISSING)), 'plain');
20
21
  process.exitCode = 1;
21
22
  return;
22
23
  }
@@ -29,10 +30,6 @@ export async function pullAction(options) {
29
30
  if (diagram.vizData) {
30
31
  const vizPath = deriveDiagramVizPathFromDBML(options.outFile);
31
32
  const existingResult = await readDiagramVizFile(vizPath);
32
- if (!existingResult.ok) {
33
- const message = getVizReadErrorMessage(existingResult.errorCode, vizPath);
34
- log.warn(message);
35
- }
36
33
  const state = fromServerFormat(diagram.vizData, existingResult.ok ? existingResult.state : undefined);
37
34
  await writeDiagramVizFile(vizPath, state);
38
35
  }
@@ -46,13 +43,7 @@ export async function pullAction(options) {
46
43
  }
47
44
  catch (error) {
48
45
  s.stop('Failed to fetch diagram.');
49
- if (isPortalApiError(error)) {
50
- log.error(getPortalApiErrorMessage(error));
51
- }
52
- else {
53
- const message = error instanceof Error ? error.message : String(error);
54
- log.error(message);
55
- }
46
+ displayError(toCliError(error), 'plain');
56
47
  process.exitCode = 1;
57
48
  }
58
49
  }