@dxos/plugin-registry 0.9.0 → 0.9.1-main.c7dcc2e112

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 (124) hide show
  1. package/dist/lib/neutral/RegistryPlugin.node.mjs +709 -18
  2. package/dist/lib/neutral/RegistryPlugin.node.mjs.map +4 -4
  3. package/dist/lib/neutral/chunk-ZY37XLVF.mjs +46 -0
  4. package/dist/lib/neutral/chunk-ZY37XLVF.mjs.map +7 -0
  5. package/dist/lib/neutral/index.mjs +1 -1
  6. package/dist/lib/neutral/meta.json +1 -1
  7. package/dist/lib/neutral/{open-plugin-registry-II735N76.mjs → open-plugin-registry-ID7LV3SF.mjs} +3 -3
  8. package/dist/lib/neutral/open-plugin-registry-ID7LV3SF.mjs.map +7 -0
  9. package/dist/lib/neutral/plugin.mjs +2 -2
  10. package/dist/lib/neutral/translations.mjs +1 -1
  11. package/dist/lib/neutral/translations.mjs.map +3 -3
  12. package/dist/types/dx.config.d.ts +28 -0
  13. package/dist/types/dx.config.d.ts.map +1 -0
  14. package/dist/types/src/capabilities/app-graph-builder.d.ts.map +1 -1
  15. package/dist/types/src/capabilities/index.d.ts +4 -12
  16. package/dist/types/src/capabilities/index.d.ts.map +1 -1
  17. package/dist/types/src/capabilities/react-surface.d.ts +2 -2
  18. package/dist/types/src/capabilities/react-surface.d.ts.map +1 -1
  19. package/dist/types/src/categories.d.ts.map +1 -1
  20. package/dist/types/src/commands/index.d.ts +2 -2
  21. package/dist/types/src/commands/index.d.ts.map +1 -1
  22. package/dist/types/src/commands/plugin/disable.d.ts.map +1 -0
  23. package/dist/types/src/commands/plugin/enable.d.ts.map +1 -0
  24. package/dist/types/src/commands/plugin/index.d.ts +3 -0
  25. package/dist/types/src/commands/plugin/index.d.ts.map +1 -0
  26. package/dist/types/src/commands/plugin/list.d.ts.map +1 -0
  27. package/dist/types/src/commands/plugin/util.d.ts.map +1 -0
  28. package/dist/types/src/commands/registry/index.d.ts +16 -0
  29. package/dist/types/src/commands/registry/index.d.ts.map +1 -0
  30. package/dist/types/src/commands/registry/publish-package.d.ts +29 -0
  31. package/dist/types/src/commands/registry/publish-package.d.ts.map +1 -0
  32. package/dist/types/src/commands/registry/publish-publisher.d.ts +19 -0
  33. package/dist/types/src/commands/registry/publish-publisher.d.ts.map +1 -0
  34. package/dist/types/src/commands/registry/publish.d.ts +25 -0
  35. package/dist/types/src/commands/registry/publish.d.ts.map +1 -0
  36. package/dist/types/src/commands/registry/records.d.ts +14 -0
  37. package/dist/types/src/commands/registry/records.d.ts.map +1 -0
  38. package/dist/types/src/commands/registry/unpublish.d.ts +15 -0
  39. package/dist/types/src/commands/registry/unpublish.d.ts.map +1 -0
  40. package/dist/types/src/commands/registry/util.d.ts +113 -0
  41. package/dist/types/src/commands/registry/util.d.ts.map +1 -0
  42. package/dist/types/src/commands/registry/verify.d.ts +20 -0
  43. package/dist/types/src/commands/registry/verify.d.ts.map +1 -0
  44. package/dist/types/src/components/PluginDetail/PluginDetail.d.ts +2 -2
  45. package/dist/types/src/components/PluginDetail/PluginDetail.d.ts.map +1 -1
  46. package/dist/types/src/components/PluginDetail/PluginDetail.stories.d.ts.map +1 -1
  47. package/dist/types/src/components/PluginList/PluginItem.d.ts.map +1 -1
  48. package/dist/types/src/components/PluginList/PluginList.stories.d.ts.map +1 -1
  49. package/dist/types/src/containers/LoadPluginDialog/LoadPluginDialog.d.ts.map +1 -1
  50. package/dist/types/src/containers/PluginArticle/PluginArticle.d.ts.map +1 -1
  51. package/dist/types/src/containers/PublicRegistryArticle/PublicRegistryArticle.d.ts.map +1 -1
  52. package/dist/types/src/hooks/useAutoTags.d.ts +2 -2
  53. package/dist/types/src/hooks/useAutoTags.d.ts.map +1 -1
  54. package/dist/types/src/hooks/useCatalogEntry.d.ts +4 -5
  55. package/dist/types/src/hooks/useCatalogEntry.d.ts.map +1 -1
  56. package/dist/types/src/hooks/usePluginActions.d.ts +3 -3
  57. package/dist/types/src/hooks/usePluginActions.d.ts.map +1 -1
  58. package/dist/types/src/hooks/useUpdateAvailable.d.ts +2 -2
  59. package/dist/types/src/hooks/useUpdateAvailable.d.ts.map +1 -1
  60. package/dist/types/src/hooks/useVersionPicker.d.ts +7 -8
  61. package/dist/types/src/hooks/useVersionPicker.d.ts.map +1 -1
  62. package/dist/types/src/meta.d.ts +28 -2
  63. package/dist/types/src/meta.d.ts.map +1 -1
  64. package/dist/types/src/operations/open-plugin-registry.d.ts.map +1 -1
  65. package/dist/types/src/types.d.ts.map +1 -1
  66. package/dist/types/tsconfig.tsbuildinfo +1 -1
  67. package/dx.config.ts +20 -0
  68. package/package.json +31 -24
  69. package/src/RegistryPlugin.node.ts +2 -2
  70. package/src/RegistryPlugin.test.ts +1 -1
  71. package/src/capabilities/app-graph-builder.ts +43 -39
  72. package/src/capabilities/dev-plugin-loader.ts +1 -1
  73. package/src/capabilities/react-surface.tsx +1 -4
  74. package/src/capabilities/settings.ts +2 -2
  75. package/src/categories.ts +5 -4
  76. package/src/commands/index.ts +2 -10
  77. package/src/commands/{disable.ts → plugin/disable.ts} +3 -3
  78. package/src/commands/{enable.ts → plugin/enable.ts} +3 -3
  79. package/src/commands/plugin/index.ts +14 -0
  80. package/src/commands/{list.ts → plugin/list.ts} +4 -4
  81. package/src/commands/registry/index.ts +30 -0
  82. package/src/commands/registry/publish-package.ts +119 -0
  83. package/src/commands/registry/publish-publisher.ts +73 -0
  84. package/src/commands/registry/publish.ts +278 -0
  85. package/src/commands/registry/records.ts +73 -0
  86. package/src/commands/registry/unpublish.ts +70 -0
  87. package/src/commands/registry/util.ts +395 -0
  88. package/src/commands/registry/verify.ts +76 -0
  89. package/src/components/DisableDependentsAlert/DisableDependentsAlert.tsx +1 -1
  90. package/src/components/PluginDetail/PluginDetail.stories.tsx +5 -3
  91. package/src/components/PluginDetail/PluginDetail.tsx +21 -23
  92. package/src/components/PluginFailureBadge/PluginFailureBadge.tsx +1 -1
  93. package/src/components/PluginList/PluginItem.tsx +5 -3
  94. package/src/components/PluginList/PluginList.stories.tsx +1 -2
  95. package/src/components/PluginList/PluginList.tsx +4 -4
  96. package/src/components/RegistrySettings/RegistrySettings.tsx +1 -1
  97. package/src/containers/BaseRegistryArticle/BaseRegistryArticle.tsx +2 -2
  98. package/src/containers/LoadPluginDialog/LoadPluginDialog.tsx +3 -3
  99. package/src/containers/PluginArticle/PluginArticle.tsx +8 -6
  100. package/src/containers/PublicRegistryArticle/PublicRegistryArticle.tsx +35 -28
  101. package/src/containers/RegistryArticle/RegistryArticle.tsx +1 -1
  102. package/src/containers/RegistrySettingsContainer/RegistrySettingsContainer.tsx +1 -1
  103. package/src/hooks/useAutoTags.ts +3 -3
  104. package/src/hooks/useCatalogEntry.ts +6 -7
  105. package/src/hooks/useDisableConfirmation.ts +1 -1
  106. package/src/hooks/usePluginActions.ts +13 -11
  107. package/src/hooks/useUpdateAvailable.ts +5 -5
  108. package/src/hooks/useVersionPicker.ts +22 -30
  109. package/src/meta.ts +6 -15
  110. package/src/operations/open-plugin-registry.ts +2 -2
  111. package/src/translations.ts +1 -1
  112. package/src/types.ts +3 -1
  113. package/dist/lib/neutral/chunk-GRQI6MLI.mjs +0 -36
  114. package/dist/lib/neutral/chunk-GRQI6MLI.mjs.map +0 -7
  115. package/dist/lib/neutral/open-plugin-registry-II735N76.mjs.map +0 -7
  116. package/dist/types/src/commands/disable.d.ts.map +0 -1
  117. package/dist/types/src/commands/enable.d.ts.map +0 -1
  118. package/dist/types/src/commands/list.d.ts.map +0 -1
  119. package/dist/types/src/commands/util.d.ts.map +0 -1
  120. /package/dist/types/src/commands/{disable.d.ts → plugin/disable.d.ts} +0 -0
  121. /package/dist/types/src/commands/{enable.d.ts → plugin/enable.d.ts} +0 -0
  122. /package/dist/types/src/commands/{list.d.ts → plugin/list.d.ts} +0 -0
  123. /package/dist/types/src/commands/{util.d.ts → plugin/util.d.ts} +0 -0
  124. /package/src/commands/{util.ts → plugin/util.ts} +0 -0
@@ -0,0 +1,73 @@
1
+ //
2
+ // Copyright 2026 DXOS.org
3
+ //
4
+
5
+ import * as Command from '@effect/cli/Command';
6
+ import * as Options from '@effect/cli/Options';
7
+ import * as FetchHttpClient from '@effect/platform/FetchHttpClient';
8
+ import * as Console from 'effect/Console';
9
+ import * as Effect from 'effect/Effect';
10
+ import * as Function from 'effect/Function';
11
+ import * as Option from 'effect/Option';
12
+
13
+ import { ClientService } from '@dxos/client';
14
+
15
+ import { AUTH_OPTION_DESCRIPTIONS, NSID, putRecord, resolveSession } from './util';
16
+
17
+ /**
18
+ * `dx registry publish-publisher` — publishes the authenticated user's own
19
+ * `org.dxos.experimental.publisher.profile` record (rkey = `self`).
20
+ *
21
+ * This is metadata about the publisher (the human/org), shown alongside their
22
+ * packages once a verifier has attested to their identity.
23
+ */
24
+ export const publishPublisher = Command.make(
25
+ 'publish-publisher',
26
+ {
27
+ handle: Options.text('handle').pipe(Options.withDescription(AUTH_OPTION_DESCRIPTIONS.handle), Options.optional),
28
+ appPassword: Options.text('app-password').pipe(
29
+ Options.withDescription(AUTH_OPTION_DESCRIPTIONS.appPassword),
30
+ Options.optional,
31
+ ),
32
+ displayName: Options.text('display-name').pipe(
33
+ Options.withDescription('Publisher display name (the human/org name shown in UIs).'),
34
+ ),
35
+ bio: Options.text('bio').pipe(Options.withDescription('Short bio.'), Options.optional),
36
+ homepageUrl: Options.text('homepage-url').pipe(
37
+ Options.withDescription('Publisher homepage URL.'),
38
+ Options.optional,
39
+ ),
40
+ contact: Options.text('contact').pipe(Options.withDescription('Contact (email, handle, etc.).'), Options.optional),
41
+ },
42
+ (options) =>
43
+ Function.pipe(
44
+ Effect.gen(function* () {
45
+ const client = yield* ClientService;
46
+ const session = yield* resolveSession({
47
+ handle: Option.getOrUndefined(options.handle),
48
+ appPassword: Option.getOrUndefined(options.appPassword),
49
+ client,
50
+ });
51
+
52
+ const record: Record<string, unknown> = {
53
+ displayName: options.displayName,
54
+ };
55
+ const bio = Option.getOrUndefined(options.bio);
56
+ if (bio !== undefined) {
57
+ record.bio = bio;
58
+ }
59
+ const homepageUrl = Option.getOrUndefined(options.homepageUrl);
60
+ if (homepageUrl !== undefined) {
61
+ record.homepageUrl = homepageUrl;
62
+ }
63
+ const contact = Option.getOrUndefined(options.contact);
64
+ if (contact !== undefined) {
65
+ record.contact = contact;
66
+ }
67
+
68
+ const result = yield* putRecord(session, NSID.PublisherProfile, 'self', record);
69
+ yield* Console.log(`Publisher ${result.uri}`);
70
+ }),
71
+ Effect.provide(FetchHttpClient.layer),
72
+ ),
73
+ ).pipe(Command.withDescription("Publish the authenticated user's publisher.profile record (rkey = self)."));
@@ -0,0 +1,278 @@
1
+ //
2
+ // Copyright 2026 DXOS.org
3
+ //
4
+
5
+ import * as Command from '@effect/cli/Command';
6
+ import * as Options from '@effect/cli/Options';
7
+ import * as PlatformCommand from '@effect/platform/Command';
8
+ import * as FetchHttpClient from '@effect/platform/FetchHttpClient';
9
+ import * as FileSystem from '@effect/platform/FileSystem';
10
+ import * as Path from '@effect/platform/Path';
11
+ import * as Console from 'effect/Console';
12
+ import * as Effect from 'effect/Effect';
13
+ import * as Function from 'effect/Function';
14
+ import * as Option from 'effect/Option';
15
+ import * as Schema from 'effect/Schema';
16
+
17
+ import { findDxConfigFile, loadDxConfig } from '@dxos/app-framework/vite-plugin';
18
+ import { type Client, ClientService } from '@dxos/client';
19
+ import { Context } from '@dxos/context';
20
+ import { EdgeHttpClient } from '@dxos/edge-client';
21
+ import { Config2 } from '@dxos/protocols';
22
+
23
+ import { AUTH_OPTION_DESCRIPTIONS, NSID, putRecord, resolveSession } from './util';
24
+
25
+ /** Manifest emitted by the build (subset consumed here). Extends `Config2.Plugin` with build-time fields. */
26
+ const ManifestSchema = Schema.Struct({
27
+ ...Config2.Plugin.fields,
28
+ version: Schema.String.pipe(Schema.nonEmptyString()),
29
+ dependencies: Schema.optional(Schema.Record({ key: Schema.String, value: Schema.String })),
30
+ });
31
+ type Manifest = Schema.Schema.Type<typeof ManifestSchema>;
32
+
33
+ const ensureTrailingSlash = (url: string): string => (url.endsWith('/') ? url : `${url}/`);
34
+
35
+ const sha256Base64 = async (bytes: Uint8Array): Promise<string> => {
36
+ const digest = await globalThis.crypto.subtle.digest('SHA-256', bytes as BufferSource);
37
+ return Buffer.from(new Uint8Array(digest)).toString('base64');
38
+ };
39
+
40
+ /**
41
+ * `dx registry publish` — config-driven build + publish.
42
+ *
43
+ * Reads the build/publish orchestration from `dx.config.ts`, runs the declared build command,
44
+ * reads the emitted `manifest.json`, hosts the bundle (default: upload the build
45
+ * output to the DXOS edge registry; override with `publish.assetBaseUrl` to point
46
+ * at your own already-hosted directory), then writes the `package.profile` and
47
+ * `package.release` records to the authenticated publisher's PDS. Release
48
+ * integrity is anchored by `manifestHash` (sha256 of `manifest.json`) in the
49
+ * signed release record.
50
+ */
51
+ export const publish = Command.make(
52
+ 'publish',
53
+ {
54
+ handle: Options.text('handle').pipe(Options.withDescription(AUTH_OPTION_DESCRIPTIONS.handle), Options.optional),
55
+ appPassword: Options.text('app-password').pipe(
56
+ Options.withDescription(AUTH_OPTION_DESCRIPTIONS.appPassword),
57
+ Options.optional,
58
+ ),
59
+ dir: Options.text('dir').pipe(
60
+ Options.withDescription('Project directory containing dx.config.ts. Defaults to the current directory.'),
61
+ Options.withDefault('.'),
62
+ ),
63
+ noBuild: Options.boolean('no-build').pipe(
64
+ Options.withDescription('Skip running the build command (publish a pre-built dist).'),
65
+ ),
66
+ assetBaseUrl: Options.text('asset-base-url').pipe(
67
+ Options.withDescription('Skip upload and point the release at an already-hosted bundle directory.'),
68
+ Options.optional,
69
+ ),
70
+ edgeUrl: Options.text('edge-url').pipe(
71
+ Options.withDescription(
72
+ 'Edge base URL for bundle upload (e.g. http://localhost:8787). Bypasses profile config; auth is skipped (requires WORKER_ENV=dev on the server).',
73
+ ),
74
+ Options.optional,
75
+ ),
76
+ },
77
+ (options) =>
78
+ Function.pipe(
79
+ Effect.gen(function* () {
80
+ const fs = yield* FileSystem.FileSystem;
81
+ const path = yield* Path.Path;
82
+ const dir = path.resolve(options.dir);
83
+
84
+ // Load + validate the build/publish orchestration from dx.config.ts.
85
+ const configFile = findDxConfigFile(dir);
86
+ if (!configFile) {
87
+ return yield* Effect.fail(new Error(`No dx.config.ts found in ${dir}.`));
88
+ }
89
+ const config = yield* Effect.tryPromise({
90
+ try: () => loadDxConfig(configFile),
91
+ catch: (error) => new Error(`Failed to load dx.config.ts in ${dir}: ${error}`),
92
+ });
93
+
94
+ // Build (unless skipped). Prepend the project's `node_modules/.bin` to PATH so
95
+ // locally-installed tools (e.g. `vite`) resolve like they do in an npm script.
96
+ const buildCommand = config.publish?.buildCommand;
97
+ if (!options.noBuild && buildCommand) {
98
+ yield* Console.log(`Building: ${buildCommand}`);
99
+ const binDir = path.join(dir, 'node_modules', '.bin');
100
+ const exitCode = yield* PlatformCommand.make(
101
+ 'sh',
102
+ '-c',
103
+ `export PATH="${binDir}:$PATH"; ${buildCommand}`,
104
+ ).pipe(
105
+ PlatformCommand.workingDirectory(dir),
106
+ PlatformCommand.stdout('inherit'),
107
+ PlatformCommand.stderr('inherit'),
108
+ PlatformCommand.exitCode,
109
+ );
110
+ if (exitCode !== 0) {
111
+ return yield* Effect.fail(new Error(`Build failed (exit ${exitCode}): ${buildCommand}`));
112
+ }
113
+ }
114
+
115
+ // Read the emitted manifest.
116
+ const outdir = path.join(dir, config.publish?.outputDirectory ?? 'dist');
117
+ const manifestPath = path.join(outdir, 'manifest.json');
118
+ if (!(yield* fs.exists(manifestPath))) {
119
+ return yield* Effect.fail(new Error(`manifest.json not found in ${outdir}. Did the build run?`));
120
+ }
121
+ const manifestRaw = yield* fs.readFileString(manifestPath);
122
+ const manifest: Manifest = yield* Schema.decodeUnknown(ManifestSchema)(JSON.parse(manifestRaw));
123
+ const key = manifest.key;
124
+ const version = manifest.version;
125
+ const manifestHash = `sha256-${yield* Effect.promise(() => sha256Base64(new TextEncoder().encode(manifestRaw)))}`;
126
+
127
+ // Resolve hosting → moduleUrl.
128
+ const assetBaseUrl = Option.getOrUndefined(options.assetBaseUrl) ?? config.publish?.assetBaseUrl;
129
+ let moduleUrl: string;
130
+ if (assetBaseUrl) {
131
+ moduleUrl = new URL('manifest.json', ensureTrailingSlash(assetBaseUrl)).toString();
132
+ yield* Console.log(`Self-hosted: ${moduleUrl}`);
133
+ } else {
134
+ // Upload to the edge registry via the authenticated edge client (hub-identity VP).
135
+ // When --edge-url is provided we bypass the profile's edge config and post directly
136
+ // with auth: false — relies on WORKER_ENV=dev skipAuth on the server (local dev only).
137
+ const explicitEdgeUrl = Option.getOrUndefined(options.edgeUrl);
138
+ if (explicitEdgeUrl) {
139
+ const http = new EdgeHttpClient(explicitEdgeUrl);
140
+ moduleUrl = yield* uploadBundleDirect({ http, key, version, outdir });
141
+ } else {
142
+ const client = yield* ClientService;
143
+ const hasIdentity = !!client.halo.identity.get();
144
+ moduleUrl = yield* uploadBundle({ client, key, version, outdir, auth: hasIdentity });
145
+ }
146
+ yield* Console.log(`Uploaded: ${moduleUrl}`);
147
+ }
148
+
149
+ // Authenticate and write records.
150
+ const client = yield* ClientService;
151
+ const session = yield* resolveSession({
152
+ handle: Option.getOrUndefined(options.handle),
153
+ appPassword: Option.getOrUndefined(options.appPassword),
154
+ client,
155
+ });
156
+ const createdAt = new Date().toISOString();
157
+
158
+ const profile: Record<string, unknown> = { key, name: manifest.name, createdAt };
159
+ if (manifest.description !== undefined) {
160
+ profile.description = manifest.description;
161
+ }
162
+ if (manifest.homePage !== undefined) {
163
+ profile.homePage = manifest.homePage;
164
+ }
165
+ if (manifest.source !== undefined) {
166
+ profile.source = manifest.source;
167
+ }
168
+ if (manifest.icon !== undefined) {
169
+ profile.icon = manifest.icon;
170
+ }
171
+ if (manifest.tags && manifest.tags.length > 0) {
172
+ profile.tags = manifest.tags;
173
+ }
174
+ if (manifest.screenshots && manifest.screenshots.length > 0) {
175
+ profile.screenshots = manifest.screenshots;
176
+ }
177
+ if (manifest.dependsOn?.length) {
178
+ profile.dependsOn = manifest.dependsOn;
179
+ }
180
+ if (manifest.spec !== undefined) {
181
+ profile.spec = manifest.spec;
182
+ }
183
+
184
+ const profileResult = yield* putRecord(session, NSID.PackageProfile, key, profile);
185
+ yield* Console.log(`Profile ${profileResult.uri}`);
186
+
187
+ const releaseResult = yield* putRecord(session, NSID.PackageRelease, `${key}:${version}`, {
188
+ package: key,
189
+ version,
190
+ moduleUrl,
191
+ manifestHash,
192
+ createdAt,
193
+ ...(manifest.dependencies ? { dependencies: manifest.dependencies } : {}),
194
+ });
195
+ yield* Console.log(`Release ${releaseResult.uri}`);
196
+ }),
197
+ Effect.provide(FetchHttpClient.layer),
198
+ ),
199
+ ).pipe(Command.withDescription('Build, host, and publish the plugin in the current directory to the registry.'));
200
+
201
+ /**
202
+ * Upload the build output to the edge registry via the authenticated edge client.
203
+ * The edge gates `/registry/upload` on the caller's hub identity (verifiable
204
+ * presentation; bypassed for local dev). Release integrity is anchored by the
205
+ * signed record's `manifestHash`, not the transport. Returns the canonical
206
+ * `moduleUrl` (the hosted `manifest.json`), targeting the client's configured edge.
207
+ */
208
+ const uploadBundle = ({
209
+ client,
210
+ key,
211
+ version,
212
+ outdir,
213
+ auth = true,
214
+ }: {
215
+ client: Client;
216
+ key: string;
217
+ version: string;
218
+ outdir: string;
219
+ auth?: boolean;
220
+ }) =>
221
+ Effect.gen(function* () {
222
+ const fs = yield* FileSystem.FileSystem;
223
+ const path = yield* Path.Path;
224
+
225
+ const entries = yield* fs.readDirectory(outdir, { recursive: true });
226
+ const files: { path: string; content: string }[] = [];
227
+ for (const entry of entries) {
228
+ const full = path.join(outdir, entry);
229
+ const info = yield* fs.stat(full);
230
+ if (info.type !== 'File') {
231
+ continue;
232
+ }
233
+ const bytes = yield* fs.readFile(full);
234
+ files.push({ path: entry.split(path.sep).join('/'), content: Buffer.from(bytes).toString('base64') });
235
+ }
236
+
237
+ const { moduleUrl } = yield* Effect.tryPromise(() =>
238
+ client.edge.http.uploadPluginBundle(Context.default(), { slug: key, version, files }, { auth }),
239
+ );
240
+ return moduleUrl;
241
+ });
242
+
243
+ /**
244
+ * Upload using a standalone EdgeHttpClient (no DXOS profile required).
245
+ * Auth is skipped — the server must have WORKER_ENV=dev to accept unauthenticated uploads.
246
+ */
247
+ const uploadBundleDirect = ({
248
+ http,
249
+ key,
250
+ version,
251
+ outdir,
252
+ }: {
253
+ http: EdgeHttpClient;
254
+ key: string;
255
+ version: string;
256
+ outdir: string;
257
+ }) =>
258
+ Effect.gen(function* () {
259
+ const fs = yield* FileSystem.FileSystem;
260
+ const path = yield* Path.Path;
261
+
262
+ const entries = yield* fs.readDirectory(outdir, { recursive: true });
263
+ const files: { path: string; content: string }[] = [];
264
+ for (const entry of entries) {
265
+ const full = path.join(outdir, entry);
266
+ const info = yield* fs.stat(full);
267
+ if (info.type !== 'File') {
268
+ continue;
269
+ }
270
+ const bytes = yield* fs.readFile(full);
271
+ files.push({ path: entry.split(path.sep).join('/'), content: Buffer.from(bytes).toString('base64') });
272
+ }
273
+
274
+ const { moduleUrl } = yield* Effect.tryPromise(() =>
275
+ http.uploadPluginBundle(Context.default(), { slug: key, version, files }, { auth: false }),
276
+ );
277
+ return moduleUrl;
278
+ });
@@ -0,0 +1,73 @@
1
+ //
2
+ // Copyright 2026 DXOS.org
3
+ //
4
+
5
+ import * as Command from '@effect/cli/Command';
6
+ import * as Options from '@effect/cli/Options';
7
+ import * as FetchHttpClient from '@effect/platform/FetchHttpClient';
8
+ import * as Console from 'effect/Console';
9
+ import * as Effect from 'effect/Effect';
10
+ import * as Function from 'effect/Function';
11
+ import * as Option from 'effect/Option';
12
+
13
+ import { CommandConfig } from '@dxos/cli-util';
14
+ import { ClientService } from '@dxos/client';
15
+
16
+ import { ALL_NSIDS, AUTH_OPTION_DESCRIPTIONS, listRecords, resolveSession, type ListRecordsEntry } from './util';
17
+
18
+ /**
19
+ * `dx registry records` — lists all `org.dxos.experimental.*` records on the
20
+ * authenticated user's repo. Useful for double-checking what's been published
21
+ * and what the registry indexer is about to ingest.
22
+ */
23
+ export const records = Command.make(
24
+ 'records',
25
+ {
26
+ handle: Options.text('handle').pipe(Options.withDescription(AUTH_OPTION_DESCRIPTIONS.handle), Options.optional),
27
+ appPassword: Options.text('app-password').pipe(
28
+ Options.withDescription(AUTH_OPTION_DESCRIPTIONS.appPassword),
29
+ Options.optional,
30
+ ),
31
+ },
32
+ (options) =>
33
+ Function.pipe(
34
+ Effect.gen(function* () {
35
+ const { json } = yield* CommandConfig;
36
+ const client = yield* ClientService;
37
+ const session = yield* resolveSession({
38
+ handle: Option.getOrUndefined(options.handle),
39
+ appPassword: Option.getOrUndefined(options.appPassword),
40
+ client,
41
+ });
42
+
43
+ // Issue listRecords against each of the four registry collections in
44
+ // parallel. The repo is tiny so the four-fan-out is fine.
45
+ const results = yield* Effect.all(
46
+ ALL_NSIDS.map((collection) =>
47
+ listRecords(session, collection).pipe(
48
+ Effect.map(({ records: entries }) => ({ collection, records: entries })),
49
+ // Empty collections come back as a 200 with `records: []`; if the
50
+ // PDS responds 400 because we've never written that collection we
51
+ // still want the others to render.
52
+ Effect.catchAll(() => Effect.succeed({ collection, records: [] as readonly ListRecordsEntry[] })),
53
+ ),
54
+ ),
55
+ { concurrency: 'unbounded' },
56
+ );
57
+
58
+ if (json) {
59
+ yield* Console.log(JSON.stringify({ did: session.did, results }, null, 2));
60
+ return;
61
+ }
62
+
63
+ yield* Console.log(`Repo: ${session.did} (${session.handle})`);
64
+ for (const { collection, records: entries } of results) {
65
+ yield* Console.log(`\n${collection} (${entries.length})`);
66
+ for (const entry of entries) {
67
+ yield* Console.log(` ${entry.uri}`);
68
+ }
69
+ }
70
+ }),
71
+ Effect.provide(FetchHttpClient.layer),
72
+ ),
73
+ ).pipe(Command.withDescription("List org.dxos.experimental.* records on the authenticated user's repo."));
@@ -0,0 +1,70 @@
1
+ //
2
+ // Copyright 2026 DXOS.org
3
+ //
4
+
5
+ import * as Command from '@effect/cli/Command';
6
+ import * as Options from '@effect/cli/Options';
7
+ import * as FetchHttpClient from '@effect/platform/FetchHttpClient';
8
+ import * as Console from 'effect/Console';
9
+ import * as Effect from 'effect/Effect';
10
+ import * as Function from 'effect/Function';
11
+ import * as Option from 'effect/Option';
12
+
13
+ import { ClientService } from '@dxos/client';
14
+
15
+ import { AUTH_OPTION_DESCRIPTIONS, NSID, deleteRecord, listRecords, resolveSession } from './util';
16
+
17
+ const rkeyOf = (uri: string): string => uri.split('/').pop() ?? '';
18
+
19
+ /**
20
+ * `dx registry unpublish` — removes a package from the authenticated user's PDS:
21
+ * deletes the `package.profile` (rkey = key) and every `package.release`
22
+ * (rkey = `<key>:<version>`). The registry indexer unindexes on its next sweep.
23
+ * Hosted bundles in edge R2 are immutable and are not removed.
24
+ */
25
+ export const unpublish = Command.make(
26
+ 'unpublish',
27
+ {
28
+ handle: Options.text('handle').pipe(Options.withDescription(AUTH_OPTION_DESCRIPTIONS.handle), Options.optional),
29
+ appPassword: Options.text('app-password').pipe(
30
+ Options.withDescription(AUTH_OPTION_DESCRIPTIONS.appPassword),
31
+ Options.optional,
32
+ ),
33
+ key: Options.text('key').pipe(
34
+ Options.withDescription('Package key (the profile rkey). Removes its profile and all releases.'),
35
+ ),
36
+ },
37
+ (options) =>
38
+ Function.pipe(
39
+ Effect.gen(function* () {
40
+ const client = yield* ClientService;
41
+ const session = yield* resolveSession({
42
+ handle: Option.getOrUndefined(options.handle),
43
+ appPassword: Option.getOrUndefined(options.appPassword),
44
+ client,
45
+ });
46
+ const key = options.key;
47
+
48
+ // Delete every release whose rkey is `<key>:<version>`, paging until exhausted.
49
+ let cursor: string | undefined;
50
+ let deletedReleases = 0;
51
+ do {
52
+ const releases = yield* listRecords(session, NSID.PackageRelease, { limit: 100, cursor });
53
+ cursor = releases.cursor;
54
+ for (const record of releases.records) {
55
+ const rkey = rkeyOf(record.uri);
56
+ if (rkey.startsWith(`${key}:`)) {
57
+ yield* deleteRecord(session, NSID.PackageRelease, rkey);
58
+ yield* Console.log(`Deleted release ${rkey}`);
59
+ deletedReleases += 1;
60
+ }
61
+ }
62
+ } while (cursor !== undefined);
63
+
64
+ // Delete the profile (rkey = key).
65
+ yield* deleteRecord(session, NSID.PackageProfile, key);
66
+ yield* Console.log(`Deleted profile ${key} (${deletedReleases} release(s) removed)`);
67
+ }),
68
+ Effect.provide(FetchHttpClient.layer),
69
+ ),
70
+ ).pipe(Command.withDescription("Remove a package (profile + releases) from the authenticated user's PDS."));