@alchemy.run/pkg 0.0.0-placeholder → 2.0.0-beta.78

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 (70) hide show
  1. package/LICENSE +201 -0
  2. package/NOTICE +5 -0
  3. package/README.md +117 -0
  4. package/dist/Manifest.d.ts +107 -0
  5. package/dist/Manifest.d.ts.map +1 -0
  6. package/dist/Manifest.js +58 -0
  7. package/dist/Manifest.js.map +1 -0
  8. package/dist/Protocol.d.ts +186 -0
  9. package/dist/Protocol.d.ts.map +1 -0
  10. package/dist/Protocol.js +115 -0
  11. package/dist/Protocol.js.map +1 -0
  12. package/dist/Registry/Bindings.d.ts +134 -0
  13. package/dist/Registry/Bindings.d.ts.map +1 -0
  14. package/dist/Registry/Bindings.js +92 -0
  15. package/dist/Registry/Bindings.js.map +1 -0
  16. package/dist/Registry/GitHub.d.ts +96 -0
  17. package/dist/Registry/GitHub.d.ts.map +1 -0
  18. package/dist/Registry/GitHub.js +226 -0
  19. package/dist/Registry/GitHub.js.map +1 -0
  20. package/dist/Registry/Handler.d.ts +53 -0
  21. package/dist/Registry/Handler.d.ts.map +1 -0
  22. package/dist/Registry/Handler.js +375 -0
  23. package/dist/Registry/Handler.js.map +1 -0
  24. package/dist/Registry/KVCache.d.ts +24 -0
  25. package/dist/Registry/KVCache.d.ts.map +1 -0
  26. package/dist/Registry/KVCache.js +42 -0
  27. package/dist/Registry/KVCache.js.map +1 -0
  28. package/dist/Registry/Sweep.d.ts +12 -0
  29. package/dist/Registry/Sweep.d.ts.map +1 -0
  30. package/dist/Registry/Sweep.js +69 -0
  31. package/dist/Registry/Sweep.js.map +1 -0
  32. package/dist/Registry/Tags.d.ts +49 -0
  33. package/dist/Registry/Tags.d.ts.map +1 -0
  34. package/dist/Registry/Tags.js +80 -0
  35. package/dist/Registry/Tags.js.map +1 -0
  36. package/dist/Registry.d.ts +378 -0
  37. package/dist/Registry.d.ts.map +1 -0
  38. package/dist/Registry.js +119 -0
  39. package/dist/Registry.js.map +1 -0
  40. package/dist/cli/bin.d.ts +18 -0
  41. package/dist/cli/bin.d.ts.map +1 -0
  42. package/dist/cli/bin.js +33 -0
  43. package/dist/cli/bin.js.map +1 -0
  44. package/dist/cli/pack.d.ts +169 -0
  45. package/dist/cli/pack.d.ts.map +1 -0
  46. package/dist/cli/pack.js +447 -0
  47. package/dist/cli/pack.js.map +1 -0
  48. package/dist/cli/publish.d.ts +33 -0
  49. package/dist/cli/publish.d.ts.map +1 -0
  50. package/dist/cli/publish.js +76 -0
  51. package/dist/cli/publish.js.map +1 -0
  52. package/dist/index.d.ts +3 -0
  53. package/dist/index.d.ts.map +1 -0
  54. package/dist/index.js +6 -0
  55. package/dist/index.js.map +1 -0
  56. package/migrations/0001_init.sql +19 -0
  57. package/package.json +89 -1
  58. package/src/Manifest.ts +76 -0
  59. package/src/Protocol.ts +163 -0
  60. package/src/Registry/Bindings.ts +134 -0
  61. package/src/Registry/GitHub.ts +365 -0
  62. package/src/Registry/Handler.ts +549 -0
  63. package/src/Registry/KVCache.ts +68 -0
  64. package/src/Registry/Sweep.ts +117 -0
  65. package/src/Registry/Tags.ts +117 -0
  66. package/src/Registry.ts +197 -0
  67. package/src/cli/bin.ts +101 -0
  68. package/src/cli/pack.ts +603 -0
  69. package/src/cli/publish.ts +129 -0
  70. package/src/index.ts +5 -0
@@ -0,0 +1,117 @@
1
+ import * as Cloudflare from "alchemy/Cloudflare";
2
+ import * as Clock from "effect/Clock";
3
+ import * as Duration from "effect/Duration";
4
+ import * as Effect from "effect/Effect";
5
+ import * as Option from "effect/Option";
6
+ import * as Stream from "effect/Stream";
7
+ import {
8
+ Bucket,
9
+ ORPHAN_GRACE,
10
+ RegistryConfig,
11
+ SWEEP_LOOKAHEAD,
12
+ tarballKey,
13
+ } from "./Bindings.ts";
14
+ import * as GitHub from "./GitHub.ts";
15
+ import * as Tags from "./Tags.ts";
16
+
17
+ /**
18
+ * Expiry sweep. Tags tied to pull requests are extended while any of those
19
+ * pull requests is open and pinned to the latest close time plus TTL once
20
+ * they all close. Expired tags go away, and tarballs nothing points at are
21
+ * deleted from R2.
22
+ */
23
+ export const sweep = Effect.gen(function* () {
24
+ const { policy } = yield* RegistryConfig;
25
+ const r2 = yield* Cloudflare.R2.ReadWriteBucket(Bucket);
26
+ const github = yield* GitHub.GitHubApp;
27
+
28
+ const now = yield* Clock.currentTimeMillis;
29
+ const ttl = Duration.toMillis(policy.ttl);
30
+ const due = yield* Tags.dueLinked(now + Duration.toMillis(SWEEP_LOOKAHEAD));
31
+
32
+ // One lookup per pull request across every due row. `undefined` means the
33
+ // pull request is still open; a failed lookup is recorded as such.
34
+ const lookups = yield* Effect.forEach(
35
+ new Set(due.flatMap((row) => row.linked_prs)),
36
+ (ref) => {
37
+ const [repo, number] = ref.split("#");
38
+ return github.getPullRequest(repo!, Number(number)).pipe(
39
+ Effect.map((pr) =>
40
+ pr.state === "open"
41
+ ? undefined
42
+ : Date.parse(pr.merged_at ?? pr.closed_at ?? "") || now,
43
+ ),
44
+ Effect.map((closedAt) => [ref, { closedAt }] as const),
45
+ Effect.catch((e) =>
46
+ Effect.logWarning(
47
+ `pull request ${ref} lookup failed: ${GitHub.describe(e)}`,
48
+ ).pipe(Effect.as([ref, undefined] as const)),
49
+ ),
50
+ );
51
+ },
52
+ { concurrency: 4 },
53
+ );
54
+ const results = new Map(lookups);
55
+ const extended = yield* Effect.forEach(due, (row) => {
56
+ const known = row.linked_prs.flatMap((ref) => {
57
+ const result = results.get(ref);
58
+ return result === undefined ? [] : [result.closedAt];
59
+ });
60
+ // Open pull requests dominate; otherwise the latest close wins. A row
61
+ // whose pull requests could not all be checked is kept past the next
62
+ // sweep rather than left to expire: it is re-checked, never deleted,
63
+ // on GitHub's bad day.
64
+ const closes = known.filter((closedAt) => closedAt !== undefined);
65
+ const expiresAt =
66
+ closes.length < known.length
67
+ ? now + ttl
68
+ : known.length < row.linked_prs.length
69
+ ? Math.max(row.expires_at, now + Duration.toMillis(SWEEP_LOOKAHEAD))
70
+ : Math.max(...closes) + ttl;
71
+ return expiresAt === row.expires_at
72
+ ? Effect.succeed(false)
73
+ : Tags.setExpiry(row.package, row.tag, expiresAt).pipe(Effect.as(true));
74
+ }).pipe(Effect.map((changed) => changed.filter(Boolean).length));
75
+
76
+ const removed = yield* Tags.deleteExpired(now);
77
+ const referenced = yield* Tags.referencedTarballs();
78
+ const unreferenced = removed.filter(
79
+ ({ package: pkg, sha256 }) => !referenced.has(`${pkg}/${sha256}`),
80
+ );
81
+ yield* Effect.forEach(
82
+ unreferenced,
83
+ ({ package: pkg, sha256 }) => r2.delete(tarballKey(pkg, sha256)),
84
+ { discard: true },
85
+ );
86
+
87
+ // Uploads that never got tagged.
88
+ const orphans = yield* Stream.paginate(
89
+ undefined as string | undefined,
90
+ (cursor) =>
91
+ r2
92
+ .list({ cursor, limit: 500 })
93
+ .pipe(
94
+ Effect.map(
95
+ (page) =>
96
+ [
97
+ page.objects,
98
+ page.truncated ? Option.some(page.cursor) : Option.none(),
99
+ ] as const,
100
+ ),
101
+ ),
102
+ ).pipe(
103
+ Stream.filter((object) => {
104
+ const match = object.key.match(/^(.+)\/([a-f0-9]{64})\.tgz$/);
105
+ return (
106
+ match !== null &&
107
+ !referenced.has(`${decodeURIComponent(match[1]!)}/${match[2]}`) &&
108
+ object.uploaded.getTime() < now - Duration.toMillis(ORPHAN_GRACE)
109
+ );
110
+ }),
111
+ Stream.mapEffect((object) => r2.delete(object.key)),
112
+ Stream.runCount,
113
+ );
114
+ yield* Effect.logInfo(
115
+ `sweep: ${due.length} due, ${extended} extended, ${removed.length} tags expired, ${unreferenced.length + orphans} tarballs deleted`,
116
+ );
117
+ });
@@ -0,0 +1,117 @@
1
+ import * as Effect from "effect/Effect";
2
+ import * as Schema from "effect/Schema";
3
+ import * as SqlClient from "effect/unstable/sql/SqlClient";
4
+
5
+ /**
6
+ * The `tags` table: one row per `(package, tag)` pointing at a tarball hash.
7
+ * A tarball lives in R2 for as long as any row points at it. Every query
8
+ * runs against the ambient `SqlClient`, which the Worker provides from its
9
+ * D1 binding.
10
+ */
11
+
12
+ const LinkedPrs = Schema.fromJsonString(Schema.Array(Schema.String));
13
+
14
+ export const TagRow = Schema.Struct({
15
+ package: Schema.String,
16
+ tag: Schema.String,
17
+ sha256: Schema.String,
18
+ expires_at: Schema.Number,
19
+ /** Pull requests whose runs produced this tag, as `owner/repo#N`. */
20
+ linked_prs: LinkedPrs,
21
+ });
22
+ export type TagRow = typeof TagRow.Type;
23
+
24
+ const TarballRow = Schema.Struct({
25
+ package: Schema.String,
26
+ sha256: Schema.String,
27
+ });
28
+
29
+ const decodeTags = Schema.decodeUnknownEffect(Schema.Array(TagRow));
30
+ const decodeTarballs = Schema.decodeUnknownEffect(Schema.Array(TarballRow));
31
+
32
+ export const get = Effect.fn("Tags.get")(function* (pkg: string, tag: string) {
33
+ const sql = yield* SqlClient.SqlClient;
34
+ const rows = yield* decodeTags(
35
+ yield* sql`SELECT * FROM tags WHERE package = ${pkg} AND tag = ${tag}`,
36
+ );
37
+ return rows[0];
38
+ });
39
+
40
+ /**
41
+ * Point `tag` at `sha256`, keeping the row alive for at least `expiresAt`
42
+ * and remembering every pull request that produced it. One statement, so
43
+ * concurrent publications of a shared tag merge rather than overwrite.
44
+ */
45
+ export const upsert = Effect.fn("Tags.upsert")(function* (input: {
46
+ readonly package: string;
47
+ readonly tag: string;
48
+ readonly sha256: string;
49
+ readonly expiresAt: number;
50
+ readonly prs: ReadonlyArray<string>;
51
+ }) {
52
+ const sql = yield* SqlClient.SqlClient;
53
+ const prs = yield* Schema.encodeEffect(LinkedPrs)(input.prs);
54
+ yield* sql`
55
+ INSERT INTO tags (package, tag, sha256, expires_at, linked_prs)
56
+ VALUES (${input.package}, ${input.tag}, ${input.sha256}, ${input.expiresAt}, ${prs})
57
+ ON CONFLICT (package, tag) DO UPDATE SET
58
+ sha256 = excluded.sha256,
59
+ expires_at = max(tags.expires_at, excluded.expires_at),
60
+ linked_prs = (
61
+ SELECT json_group_array(value) FROM (
62
+ SELECT value FROM json_each(tags.linked_prs)
63
+ UNION
64
+ SELECT value FROM json_each(excluded.linked_prs)
65
+ )
66
+ )
67
+ `;
68
+ });
69
+
70
+ export const resolve = (pkg: string, tag: string) =>
71
+ Effect.map(get(pkg, tag), (row) => row?.sha256);
72
+
73
+ /** Rows tied to pull requests whose expiry falls before `before`. */
74
+ export const dueLinked = Effect.fn("Tags.dueLinked")(function* (
75
+ before: number,
76
+ ) {
77
+ const sql = yield* SqlClient.SqlClient;
78
+ return yield* decodeTags(
79
+ yield* sql`
80
+ SELECT * FROM tags WHERE linked_prs != '[]' AND expires_at < ${before}
81
+ `,
82
+ );
83
+ });
84
+
85
+ export const setExpiry = Effect.fn("Tags.setExpiry")(function* (
86
+ pkg: string,
87
+ tag: string,
88
+ expiresAt: number,
89
+ ) {
90
+ const sql = yield* SqlClient.SqlClient;
91
+ yield* sql`
92
+ UPDATE tags SET expires_at = ${expiresAt} WHERE package = ${pkg} AND tag = ${tag}
93
+ `;
94
+ });
95
+
96
+ /** Delete every expired row and return the tarballs those rows pointed at. */
97
+ export const deleteExpired = Effect.fn("Tags.deleteExpired")(function* (
98
+ now: number,
99
+ ) {
100
+ const sql = yield* SqlClient.SqlClient;
101
+ return yield* decodeTarballs(
102
+ yield* sql`
103
+ DELETE FROM tags WHERE expires_at < ${now} RETURNING package, sha256
104
+ `,
105
+ );
106
+ });
107
+
108
+ /** Every tarball some tag still points at, as `<package>/<sha256>`. */
109
+ export const referencedTarballs = Effect.fn("Tags.referencedTarballs")(
110
+ function* () {
111
+ const sql = yield* SqlClient.SqlClient;
112
+ const rows = yield* decodeTarballs(
113
+ yield* sql`SELECT DISTINCT package, sha256 FROM tags`,
114
+ );
115
+ return new Set(rows.map((row) => `${row.package}/${row.sha256}`));
116
+ },
117
+ );
@@ -0,0 +1,197 @@
1
+ import * as Cloudflare from "alchemy/Cloudflare";
2
+ import * as Namespace from "alchemy/Namespace";
3
+ import type * as ByteSize from "effect/ByteSize";
4
+ import * as Config from "effect/Config";
5
+ import type * as Duration from "effect/Duration";
6
+ import * as Effect from "effect/Effect";
7
+ import * as Layer from "effect/Layer";
8
+ import * as Schema from "effect/Schema";
9
+ import {
10
+ APP_ID_ENV,
11
+ DEFAULT_API_URL,
12
+ DEFAULT_CRON,
13
+ DEFAULT_TTL,
14
+ PRIVATE_KEY_ENV,
15
+ RegistryConfig,
16
+ SETTINGS_ENV,
17
+ Settings,
18
+ type GitHubCredentials,
19
+ type WorkerOverrides,
20
+ } from "./Registry/Bindings.ts";
21
+ import { handler } from "./Registry/Handler.ts";
22
+
23
+ export {
24
+ Bucket,
25
+ Cache,
26
+ Database,
27
+ Policy,
28
+ RegistryConfig,
29
+ Settings,
30
+ } from "./Registry/Bindings.ts";
31
+
32
+ export interface RegistryOptions {
33
+ /**
34
+ * Worker props forwarded verbatim: `domain`, `name`, `compatibility`,
35
+ * `observability`, `access`, and so on. `main`, `env`, and `crons` are
36
+ * owned by the registry.
37
+ */
38
+ readonly worker?: WorkerOverrides;
39
+ /** GitHub App credentials, read at deploy time and bound to the Worker. */
40
+ readonly github: GitHubCredentials & {
41
+ /** @default "https://api.github.com" */
42
+ readonly apiUrl?: string;
43
+ };
44
+ readonly policy: {
45
+ /** Repositories allowed to publish, as `owner/name`. */
46
+ readonly repos: ReadonlyArray<string>;
47
+ /** @default Duration.weeks(1) */
48
+ readonly ttl?: Duration.Duration;
49
+ /** Upper bound on a single tarball. Absent means unlimited. */
50
+ readonly maxPackageSize?: ByteSize.ByteSize;
51
+ };
52
+ /**
53
+ * Hostname to package scope. Requests on an aliased host resolve an
54
+ * unscoped name under that scope, so `pkg.distilled.cloud/core/<sha>`
55
+ * serves `@distilled.cloud/core`.
56
+ */
57
+ readonly aliases?: Readonly<Record<string, string>>;
58
+ /**
59
+ * Cron expression for the expiry sweep.
60
+ * @default "0 * * * *"
61
+ */
62
+ readonly cron?: string;
63
+ }
64
+
65
+ /**
66
+ * The registry Worker. Its props are an Effect so the same declaration
67
+ * serves both sides of the bundle boundary: at plan time
68
+ * {@link RegistryConfig} is the caller's options, inside the isolate it is
69
+ * rebuilt from the Worker's bindings.
70
+ */
71
+ export class Registry extends Cloudflare.Worker<Registry, {}>()("Worker") {}
72
+
73
+ const RegistryLive = Registry.make(
74
+ Effect.gen(function* () {
75
+ const config = yield* RegistryConfig;
76
+ return {
77
+ ...config.worker,
78
+ main: import.meta.url,
79
+ env: {
80
+ [SETTINGS_ENV]: yield* Schema.encodeEffect(Settings)({
81
+ policy: config.policy,
82
+ aliases: config.aliases,
83
+ cron: config.cron,
84
+ github: { apiUrl: config.github.apiUrl },
85
+ }).pipe(Effect.orDie),
86
+ [APP_ID_ENV]: config.github.appId,
87
+ [PRIVATE_KEY_ENV]: config.github.privateKey,
88
+ },
89
+ };
90
+ }),
91
+ handler,
92
+ );
93
+
94
+ /**
95
+ * Runtime side of {@link RegistryConfig}: decode the settings binding and
96
+ * point the credentials at the bindings `RegistryLive` declared.
97
+ */
98
+ const RegistryConfigFromEnv = Layer.effect(
99
+ RegistryConfig,
100
+ Effect.gen(function* () {
101
+ const env = yield* Cloudflare.Workers.WorkerEnvironment;
102
+ const settings = yield* Schema.decodeUnknownEffect(Settings)(
103
+ env[SETTINGS_ENV],
104
+ ).pipe(Effect.orDie);
105
+ return {
106
+ ...settings,
107
+ worker: {},
108
+ github: {
109
+ ...settings.github,
110
+ appId: Config.String(APP_ID_ENV),
111
+ privateKey: Config.Redacted(PRIVATE_KEY_ENV),
112
+ },
113
+ };
114
+ }),
115
+ );
116
+
117
+ /**
118
+ * The bundle entry. `RegistryLive` names this file as `main`, so the
119
+ * generated Worker entry imports this default export and builds it once per
120
+ * isolate. User code never imports it; use {@link PkgRegistry}.
121
+ */
122
+ export default RegistryLive.pipe(Layer.provide(RegistryConfigFromEnv));
123
+
124
+ /**
125
+ * A preview package registry: a Worker serving install URLs and the publish
126
+ * API, an R2 bucket of content-addressed tarballs, a D1 index of tags, and a
127
+ * KV namespace caching what the Worker learns from GitHub. They are declared
128
+ * under `id` as `<id>/Worker`, `<id>/Bucket`, `<id>/Database`, and
129
+ * `<id>/Cache`.
130
+ *
131
+ * ### Deploying a registry
132
+ * **Example:** Stack
133
+ * ```typescript
134
+ * // stacks/pkg.ts
135
+ * import { PkgRegistry } from "@alchemy.run/pkg/Registry";
136
+ * import * as Alchemy from "alchemy";
137
+ * import * as Cloudflare from "alchemy/Cloudflare";
138
+ * import * as ByteSize from "effect/ByteSize";
139
+ * import * as Config from "effect/Config";
140
+ * import * as Duration from "effect/Duration";
141
+ * import * as Effect from "effect/Effect";
142
+ *
143
+ * export default Alchemy.Stack(
144
+ * "Pkg",
145
+ * { providers: Cloudflare.providers(), state: Cloudflare.state() },
146
+ * Effect.gen(function* () {
147
+ * const registry = yield* PkgRegistry("Pkg", {
148
+ * worker: { domain: "pkg.alchemy.run" },
149
+ * github: {
150
+ * appId: Config.String("GH_APP_ID"),
151
+ * privateKey: Config.Redacted("GH_APP_PRIVATE_KEY"),
152
+ * },
153
+ * policy: {
154
+ * repos: ["alchemy-run/alchemy", "alchemy-run/distilled"],
155
+ * ttl: Duration.weeks(1),
156
+ * maxPackageSize: ByteSize.megabytes(100),
157
+ * },
158
+ * });
159
+ * return { url: registry.url.as<string>() };
160
+ * }),
161
+ * );
162
+ * ```
163
+ *
164
+ * @resource
165
+ * @product Workers
166
+ * @category Workers & Compute
167
+ */
168
+ export const PkgRegistry = <const Id extends string>(
169
+ id: Id,
170
+ options: RegistryOptions,
171
+ ) =>
172
+ Registry.pipe(
173
+ Effect.provide(
174
+ RegistryLive.pipe(
175
+ Layer.provide(
176
+ Layer.succeed(RegistryConfig, {
177
+ worker: options.worker ?? {},
178
+ policy: {
179
+ repos: options.policy.repos,
180
+ ttl: options.policy.ttl ?? DEFAULT_TTL,
181
+ ...(options.policy.maxPackageSize !== undefined
182
+ ? { maxPackageSize: options.policy.maxPackageSize }
183
+ : {}),
184
+ },
185
+ aliases: options.aliases ?? {},
186
+ cron: options.cron ?? DEFAULT_CRON,
187
+ github: {
188
+ apiUrl: options.github.apiUrl ?? DEFAULT_API_URL,
189
+ appId: options.github.appId,
190
+ privateKey: options.github.privateKey,
191
+ },
192
+ }),
193
+ ),
194
+ ),
195
+ ),
196
+ Namespace.push(id),
197
+ );
package/src/cli/bin.ts ADDED
@@ -0,0 +1,101 @@
1
+ #!/usr/bin/env node
2
+ import { PlatformServices, runMain } from "alchemy/Util/PlatformServices";
3
+ import * as Config from "effect/Config";
4
+ import * as Effect from "effect/Effect";
5
+ import * as Layer from "effect/Layer";
6
+ import {
7
+ CliConfig,
8
+ CliError,
9
+ Command,
10
+ Flag,
11
+ GlobalFlag,
12
+ } from "effect/unstable/cli";
13
+ import * as FetchHttpClient from "effect/unstable/http/FetchHttpClient";
14
+ import pkg from "../../package.json" with { type: "json" };
15
+ import { Group, pack } from "./pack.ts";
16
+ import { publish } from "./publish.ts";
17
+
18
+ const groupFlag = Flag.String("group").pipe(
19
+ Flag.withDescription(
20
+ "Display group and directory glob, as NAME=GLOB or NAME[Collapsed]=GLOB (repeatable), e.g. --group Alchemy=./packages/*",
21
+ ),
22
+ Flag.withSchema(Group),
23
+ Flag.atLeast(1),
24
+ );
25
+
26
+ const registryFlag = Flag.String("registry").pipe(
27
+ Flag.withDescription(
28
+ "Registry origin used for install URLs. Falls back to PKG_REGISTRY, then https://pkg.alchemy.run",
29
+ ),
30
+ Flag.withFallbackConfig(Config.String("PKG_REGISTRY")),
31
+ Flag.withDefault("https://pkg.alchemy.run"),
32
+ );
33
+
34
+ const outFlag = Flag.String("out").pipe(
35
+ Flag.withDescription("Directory to write tarballs and the manifest into"),
36
+ Flag.withDefault(".pkg"),
37
+ );
38
+
39
+ export const packCommand = Command.make(
40
+ "pack",
41
+ { group: groupFlag, registry: registryFlag, out: outFlag },
42
+ ({ group, registry, out }) =>
43
+ Effect.gen(function* () {
44
+ const cwd = yield* Effect.sync(() => process.cwd());
45
+ yield* pack({ cwd, groups: group, registry, out });
46
+ }),
47
+ ).pipe(
48
+ Command.withDescription(
49
+ "Pack workspace packages into reproducible tarballs with dependencies rewritten to registry URLs",
50
+ ),
51
+ Command.withExamples([
52
+ {
53
+ command:
54
+ "pkg pack --group 'alchemy=./packages/alchemy' --group '@alchemy.run[Collapsed]=./packages/{better-auth,pkg}' --group '@distilled.cloud[Collapsed]=./submodules/distilled/packages/*'",
55
+ },
56
+ ]),
57
+ );
58
+
59
+ const dirFlag = Flag.String("dir").pipe(
60
+ Flag.withDescription("Directory written by pkg pack"),
61
+ Flag.withDefault(".pkg"),
62
+ );
63
+
64
+ export const publishCommand = Command.make(
65
+ "publish",
66
+ { registry: registryFlag, dir: dirFlag },
67
+ ({ registry, dir }) =>
68
+ Effect.gen(function* () {
69
+ const cwd = yield* Effect.sync(() => process.cwd());
70
+ yield* publish({ cwd, dir, registry });
71
+ }),
72
+ ).pipe(
73
+ Command.withDescription(
74
+ "Publish a pkg pack directory from the current GitHub Actions job, after its manifest artifact has been uploaded",
75
+ ),
76
+ Command.withExamples([
77
+ { command: "pkg publish --registry https://pkg.alchemy.run" },
78
+ ]),
79
+ );
80
+
81
+ export const root = Command.make("pkg", {}, () =>
82
+ Effect.fail(new CliError.ShowHelp({ commandPath: ["pkg"], errors: [] })),
83
+ ).pipe(
84
+ Command.withDescription(
85
+ "Pack and publish preview packages for pull requests.",
86
+ ),
87
+ Command.withSubcommands([packCommand, publishCommand]),
88
+ );
89
+
90
+ Command.run(root, { version: pkg.version }).pipe(
91
+ Effect.provide(
92
+ Layer.mergeAll(
93
+ PlatformServices,
94
+ FetchHttpClient.layer,
95
+ CliConfig.layer({
96
+ builtIns: [GlobalFlag.Help, GlobalFlag.Version],
97
+ }),
98
+ ),
99
+ ),
100
+ runMain,
101
+ );