@dxos/cli-util 0.8.4-main.9735255 → 0.8.4-main.abd8ff62ef

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 (31) hide show
  1. package/dist/lib/node-esm/{chunk-6TKUDRM6.mjs → chunk-N5LOOWPE.mjs} +1 -1
  2. package/dist/lib/node-esm/index.mjs +106 -70
  3. package/dist/lib/node-esm/index.mjs.map +3 -3
  4. package/dist/lib/node-esm/meta.json +1 -1
  5. package/dist/lib/node-esm/testing/index.mjs +2 -2
  6. package/dist/lib/node-esm/testing/index.mjs.map +3 -3
  7. package/dist/types/src/testing/test-console.d.ts.map +1 -1
  8. package/dist/types/src/testing/test-layer.d.ts +1 -1
  9. package/dist/types/src/testing/test-layer.d.ts.map +1 -1
  10. package/dist/types/src/util/form-builder.d.ts +1 -1
  11. package/dist/types/src/util/form-builder.d.ts.map +1 -1
  12. package/dist/types/src/util/options.d.ts.map +1 -1
  13. package/dist/types/src/util/platform.d.ts.map +1 -1
  14. package/dist/types/src/util/printer.d.ts.map +1 -1
  15. package/dist/types/src/util/runtime.d.ts +1 -1
  16. package/dist/types/src/util/runtime.d.ts.map +1 -1
  17. package/dist/types/src/util/space-format.d.ts +13 -2
  18. package/dist/types/src/util/space-format.d.ts.map +1 -1
  19. package/dist/types/src/util/space.d.ts +7 -7
  20. package/dist/types/src/util/space.d.ts.map +1 -1
  21. package/dist/types/tsconfig.tsbuildinfo +1 -1
  22. package/package.json +17 -15
  23. package/src/testing/test-console.ts +1 -2
  24. package/src/testing/test-layer.ts +0 -1
  25. package/src/util/form-builder.ts +1 -1
  26. package/src/util/platform.ts +1 -2
  27. package/src/util/printer.ts +1 -1
  28. package/src/util/runtime.ts +1 -1
  29. package/src/util/space-format.ts +86 -12
  30. package/src/util/space.ts +43 -29
  31. /package/dist/lib/node-esm/{chunk-6TKUDRM6.mjs.map → chunk-N5LOOWPE.mjs.map} +0 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dxos/cli-util",
3
- "version": "0.8.4-main.9735255",
3
+ "version": "0.8.4-main.abd8ff62ef",
4
4
  "description": "Shared CLI utilities for DXOS CLI commands and plugins",
5
5
  "homepage": "https://dxos.org",
6
6
  "bugs": "https://github.com/dxos/dxos/issues",
@@ -30,26 +30,28 @@
30
30
  "src"
31
31
  ],
32
32
  "dependencies": {
33
- "@effect/cli": "0.72.1",
33
+ "@effect/cli": "0.73.2",
34
34
  "@effect/printer": "0.47.0",
35
35
  "@effect/printer-ansi": "0.47.0",
36
- "@dxos/client": "0.8.4-main.9735255",
37
- "@dxos/echo": "0.8.4-main.9735255",
38
- "@dxos/debug": "0.8.4-main.9735255",
39
- "@dxos/errors": "0.8.4-main.9735255",
40
- "@dxos/functions": "0.8.4-main.9735255",
41
- "@dxos/effect": "0.8.4-main.9735255",
42
- "@dxos/log": "0.8.4-main.9735255",
43
- "@dxos/protocols": "0.8.4-main.9735255",
44
- "@dxos/util": "0.8.4-main.9735255"
36
+ "@dxos/app-toolkit": "0.8.4-main.abd8ff62ef",
37
+ "@dxos/client": "0.8.4-main.abd8ff62ef",
38
+ "@dxos/debug": "0.8.4-main.abd8ff62ef",
39
+ "@dxos/echo": "0.8.4-main.abd8ff62ef",
40
+ "@dxos/errors": "0.8.4-main.abd8ff62ef",
41
+ "@dxos/effect": "0.8.4-main.abd8ff62ef",
42
+ "@dxos/compute": "0.8.4-main.abd8ff62ef",
43
+ "@dxos/functions": "0.8.4-main.abd8ff62ef",
44
+ "@dxos/log": "0.8.4-main.abd8ff62ef",
45
+ "@dxos/protocols": "0.8.4-main.abd8ff62ef",
46
+ "@dxos/util": "0.8.4-main.abd8ff62ef"
45
47
  },
46
48
  "devDependencies": {
47
- "effect": "3.19.11",
48
- "typescript": "^5.9.3",
49
- "vitest": "3.2.4"
49
+ "effect": "3.20.0",
50
+ "typescript": "^6.0.3",
51
+ "vitest": "4.1.5"
50
52
  },
51
53
  "peerDependencies": {
52
- "effect": "3.19.11"
54
+ "effect": "3.20.0"
53
55
  },
54
56
  "publishConfig": {
55
57
  "access": "public"
@@ -2,12 +2,11 @@
2
2
  // Copyright 2025 DXOS.org
3
3
  //
4
4
 
5
- import { inspect } from 'node:util';
6
-
7
5
  import * as Console from 'effect/Console';
8
6
  import * as Context from 'effect/Context';
9
7
  import * as Effect from 'effect/Effect';
10
8
  import * as Layer from 'effect/Layer';
9
+ import { inspect } from 'node:util';
11
10
 
12
11
  function logToString(...args: any[]): string {
13
12
  return args
@@ -8,7 +8,6 @@ import * as Layer from 'effect/Layer';
8
8
  import { ClientService, ConfigService } from '@dxos/client';
9
9
 
10
10
  import { CommandConfig } from '../services';
11
-
12
11
  import { TestConsole } from './test-console';
13
12
 
14
13
  export const TestLayer = Function.pipe(
@@ -2,8 +2,8 @@
2
2
  // Copyright 2025 DXOS.org
3
3
  //
4
4
 
5
- import * as Doc from '@effect/printer/Doc';
6
5
  import * as Ansi from '@effect/printer-ansi/Ansi';
6
+ import * as Doc from '@effect/printer/Doc';
7
7
  import * as Option from 'effect/Option';
8
8
  import * as Pipeable from 'effect/Pipeable';
9
9
 
@@ -2,9 +2,8 @@
2
2
  // Copyright 2025 DXOS.org
3
3
  //
4
4
 
5
- import { spawn } from 'node:child_process';
6
-
7
5
  import * as Effect from 'effect/Effect';
6
+ import { spawn } from 'node:child_process';
8
7
 
9
8
  /**
10
9
  * Copy text to the system clipboard.
@@ -2,8 +2,8 @@
2
2
  // Copyright 2025 DXOS.org
3
3
  //
4
4
 
5
- import * as Doc from '@effect/printer/Doc';
6
5
  import * as AnsiDoc from '@effect/printer-ansi/AnsiDoc';
6
+ import * as Doc from '@effect/printer/Doc';
7
7
 
8
8
  /**
9
9
  * Pretty print document.
@@ -8,7 +8,7 @@ import { ClientService } from '@dxos/client';
8
8
  import { type Type } from '@dxos/echo';
9
9
 
10
10
  /** @deprecated Migrate to providing types via plugin capabilities. */
11
- export const withTypes: (...types: Type.Entity.Any[]) => Effect.Effect<void, never, ClientService> = (...types) =>
11
+ export const withTypes: (...types: Type.AnyEntity[]) => Effect.Effect<void, never, ClientService> = (...types) =>
12
12
  Effect.gen(function* () {
13
13
  const client = yield* ClientService;
14
14
  yield* Effect.promise(() => client.addTypes(types));
@@ -2,40 +2,114 @@
2
2
  // Copyright 2025 DXOS.org
3
3
  //
4
4
 
5
+ import * as Duration from 'effect/Duration';
5
6
  import * as Effect from 'effect/Effect';
6
7
 
7
8
  import { type Space, SpaceState, type SpaceSyncState } from '@dxos/client/echo';
8
9
 
9
10
  import * as FormBuilder from './form-builder';
10
11
 
12
+ export type FormatSpaceOptions = {
13
+ verbose?: boolean;
14
+ truncateKeys?: boolean;
15
+ /**
16
+ * If set, wait up to this many seconds for the space to reach
17
+ * `SPACE_READY` before reading its fields. If unset, read whatever state
18
+ * is available right now — much safer for `space list` etc., where a
19
+ * single stuck space would otherwise hang the entire command.
20
+ */
21
+ waitSeconds?: number;
22
+ };
23
+
24
+ const DEFAULT_OPTIONS: Required<FormatSpaceOptions> = {
25
+ verbose: false,
26
+ truncateKeys: false,
27
+ waitSeconds: 0,
28
+ };
29
+
30
+ /**
31
+ * Per-async-read internal timeout. Some `space.internal.*` getters do
32
+ * filesystem / network IO and can themselves hang on a partially-loaded
33
+ * space; cap each one so the command can never be held hostage by SDK
34
+ * internals.
35
+ */
36
+ const READ_TIMEOUT_SECONDS = 2;
37
+
38
+ const tryWithFallback = <T>(label: string, run: () => Promise<T>, fallback: T) =>
39
+ Effect.tryPromise(run).pipe(
40
+ Effect.timeoutFail({
41
+ duration: Duration.seconds(READ_TIMEOUT_SECONDS),
42
+ onTimeout: () => new Error(`${label} timed out`),
43
+ }),
44
+ Effect.catchAll(() => Effect.succeed(fallback)),
45
+ );
46
+
47
+ const tryWithFallbackSync = <T>(read: () => T, fallback: T): T => {
48
+ try {
49
+ return read();
50
+ } catch {
51
+ return fallback;
52
+ }
53
+ };
54
+
11
55
  // TODO(wittjosiah): Use @effect/printer.
12
- export const formatSpace = Effect.fn(function* (space: Space, options = { verbose: false, truncateKeys: false }) {
13
- yield* Effect.tryPromise(() => space.waitUntilReady());
56
+ export const formatSpace = Effect.fn(function* (space: Space, options: FormatSpaceOptions = {}) {
57
+ const { waitSeconds } = { ...DEFAULT_OPTIONS, ...options };
58
+
59
+ // Opt-in wait. Defaults to NO wait so a single stuck space can't hang
60
+ // an enumeration command (e.g. `dx space list`).
61
+ if (waitSeconds > 0) {
62
+ yield* Effect.tryPromise(() => space.waitUntilReady()).pipe(
63
+ Effect.timeoutFail({
64
+ duration: Duration.seconds(waitSeconds),
65
+ onTimeout: () => new Error('waitUntilReady timed out'),
66
+ }),
67
+ Effect.catchAll(() => Effect.void),
68
+ );
69
+ }
70
+
71
+ const state = tryWithFallbackSync(() => space.state.get(), SpaceState.SPACE_INITIALIZING);
72
+ const ready = state === SpaceState.SPACE_READY;
14
73
 
15
74
  // TODO(burdon): Factor out.
16
75
  // TODO(burdon): Agent needs to restart before `ready` is available.
17
- const { open, ready } = space.internal.data.metrics ?? {};
18
- const startup = open && ready && ready.getTime() - open.getTime();
76
+ const metrics = tryWithFallbackSync(
77
+ () => space.internal.data.metrics,
78
+ undefined as { open?: Date; ready?: Date } | undefined,
79
+ );
80
+ const startup = metrics?.open && metrics?.ready ? metrics.ready.getTime() - metrics.open.getTime() : undefined;
19
81
 
20
82
  // TODO(burdon): Get feeds from client-services if verbose (factor out from devtools/diagnostics).
21
83
  // const host = client.services.services.DevtoolsHost!;
22
- const pipeline = space.internal.data.pipeline;
84
+ const pipeline = tryWithFallbackSync(() => space.internal.data.pipeline, undefined);
23
85
  const epoch = pipeline?.currentEpoch?.subject.assertion.number;
24
86
 
25
- const syncState = aggregateSyncState(yield* Effect.tryPromise(() => space.internal.db.coreDatabase.getSyncState()));
87
+ // The sync-state read does IO; cap it so a stuck space can't hang the
88
+ // command. Falls back to a "no peers" placeholder.
89
+ const syncStateRaw = yield* tryWithFallback('getSyncState', () => space.internal.db.coreDatabase.getSyncState(), {
90
+ peers: {},
91
+ } as SpaceSyncState);
92
+ const syncState = aggregateSyncState(syncStateRaw);
93
+
94
+ const name = ready ? tryWithFallbackSync(() => space.properties.name, undefined) : 'loading...';
95
+ const members = tryWithFallbackSync(() => space.members.get().length, 0);
96
+ const objects = tryWithFallbackSync(() => space.internal.db.coreDatabase.getAllObjectIds().length, 0);
97
+ const key = options.truncateKeys
98
+ ? tryWithFallbackSync(() => space.key.truncate(), '')
99
+ : tryWithFallbackSync(() => space.key.toHex(), '');
26
100
 
27
101
  return {
28
102
  id: space.id,
29
- state: SpaceState[space.state.get()],
30
- name: space.state.get() === SpaceState.SPACE_READY ? space.properties.name : 'loading...',
103
+ state: SpaceState[state],
104
+ name,
31
105
 
32
- members: space.members.get().length,
33
- objects: space.internal.db.coreDatabase.getAllObjectIds().length,
106
+ members,
107
+ objects,
34
108
 
35
- key: options.truncateKeys ? space.key.truncate() : space.key.toHex(),
109
+ key,
36
110
  epoch,
37
111
  startup,
38
- automergeRoot: space.internal.data.pipeline?.spaceRootUrl,
112
+ automergeRoot: pipeline?.spaceRootUrl,
39
113
  // appliedEpoch,
40
114
  syncState: `${syncState.count} ${getSyncIndicator(syncState.up, syncState.down)} (${syncState.peers} peers)`,
41
115
  };
package/src/util/space.ts CHANGED
@@ -5,9 +5,9 @@
5
5
  import * as Console from 'effect/Console';
6
6
  import * as Effect from 'effect/Effect';
7
7
  import * as Layer from 'effect/Layer';
8
- import * as Match from 'effect/Match';
9
8
  import * as Option from 'effect/Option';
10
9
 
10
+ import { getPersonalSpace } from '@dxos/app-toolkit';
11
11
  import { ClientService } from '@dxos/client';
12
12
  import { type Space } from '@dxos/client/echo';
13
13
  import { Database, type Key } from '@dxos/echo';
@@ -26,34 +26,42 @@ export const getSpace = (spaceId: Key.SpaceId): Effect.Effect<Space, SpaceNotFou
26
26
  export const spaceIdWithDefault = (spaceId: Option.Option<Key.SpaceId>) =>
27
27
  Effect.gen(function* () {
28
28
  const client = yield* ClientService;
29
- yield* Effect.promise(() => client.spaces.waitUntilReady());
30
- return Option.getOrElse(spaceId, () => client.spaces.default.id);
29
+ return Option.getOrElse(spaceId, () => {
30
+ const personal = getPersonalSpace(client);
31
+ if (!personal) {
32
+ throw new Error('No space ID provided and no personal space found.');
33
+ }
34
+ return personal.id;
35
+ });
31
36
  });
32
37
 
33
38
  // TODO(wittjosiah): Factor out.
34
39
  export const spaceLayer = (
35
40
  spaceId$: Option.Option<Key.SpaceId>,
36
- fallbackToDefaultSpace = false,
41
+ fallbackToPersonalSpace = false,
37
42
  ): Layer.Layer<Database.Service | QueueService, never, ClientService> => {
38
43
  const getSpace = Effect.fn(function* () {
39
44
  const client = yield* ClientService;
40
- yield* Effect.promise(() => client.spaces.waitUntilReady());
41
-
42
- const spaceId = Match.value(fallbackToDefaultSpace).pipe(
43
- Match.when(true, () =>
44
- spaceId$.pipe(
45
- Option.getOrElse(() => client.spaces.default.id),
46
- Option.some,
47
- ),
48
- ),
49
- Match.when(false, () => spaceId$),
50
- Match.exhaustive,
51
- );
52
-
53
- const space = spaceId.pipe(
54
- Option.flatMap((id) => Option.fromNullable(client.spaces.get(id))),
55
- Option.getOrUndefined,
56
- );
45
+
46
+ // Resolution order when fallbackToPersonalSpace is true:
47
+ // 1. the explicit spaceId arg (if provided);
48
+ // 2. the space tagged `org.dxos.space.personal`;
49
+ // 3. the first available space.
50
+ // This keeps profiles created outside composer-app (which is what creates
51
+ // the personal-space tag on identity creation) usable — the alternative
52
+ // is a "Space not found" throw deep inside CredentialsService.
53
+ const resolveSpace = () => {
54
+ if (!fallbackToPersonalSpace) {
55
+ return spaceId$.pipe(Option.flatMap((id) => Option.fromNullable(client.spaces.get(id))));
56
+ }
57
+ return spaceId$.pipe(
58
+ Option.flatMap((id) => Option.fromNullable(client.spaces.get(id))),
59
+ Option.orElse(() => Option.fromNullable(getPersonalSpace(client))),
60
+ Option.orElse(() => Option.fromNullable(client.spaces.get()[0])),
61
+ );
62
+ };
63
+
64
+ const space = resolveSpace().pipe(Option.getOrUndefined);
57
65
 
58
66
  if (space) {
59
67
  yield* Effect.promise(() => space.waitUntilReady());
@@ -61,21 +69,27 @@ export const spaceLayer = (
61
69
  return space;
62
70
  });
63
71
 
72
+ // When no space can be resolved we install a stub whose `db` getter throws
73
+ // on access — preserves the existing semantics for commands that *do* need
74
+ // a db — but the release callback must NOT touch `db` or it will throw
75
+ // during teardown (e.g. after a command emits a friendly error and
76
+ // returns early). A shared sentinel object short-circuits the release.
77
+ const NO_DB_STUB = {
78
+ get db(): Database.Database {
79
+ throw new Error('Space not found');
80
+ },
81
+ };
64
82
  const db = Layer.scoped(
65
83
  Database.Service,
66
84
  Effect.acquireRelease(
67
85
  Effect.gen(function* () {
68
86
  const space = yield* getSpace();
69
87
  if (!space) {
70
- return {
71
- get db(): Database.Database {
72
- throw new Error('Space not found');
73
- },
74
- };
88
+ return NO_DB_STUB;
75
89
  }
76
90
  return { db: space.db };
77
91
  }),
78
- ({ db }) => Effect.promise(() => db.flush({ indexes: true })),
92
+ (holder) => (holder === NO_DB_STUB ? Effect.void : Effect.promise(() => holder.db.flush())),
79
93
  ),
80
94
  );
81
95
 
@@ -129,8 +143,8 @@ export const waitForSync = Effect.fn(function* (space: Space) {
129
143
  });
130
144
 
131
145
  export const flushAndSync = Effect.fn(function* (opts?: Database.FlushOptions) {
132
- yield* Database.Service.flush(opts);
133
- const spaceId = yield* Database.Service.spaceId;
146
+ yield* Database.flush(opts);
147
+ const spaceId = yield* Database.spaceId;
134
148
  const space = yield* getSpace(spaceId);
135
149
  yield* waitForSync(space);
136
150
  });