@hitch42/applet 0.1.1-beta.6 → 0.1.1-beta.8

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 (51) hide show
  1. package/dist/_internal/server.d.ts +14 -1
  2. package/dist/_internal/server.js +23 -74
  3. package/dist/applet.js +50 -29
  4. package/dist/build-CFCKKMn7.js +2 -0
  5. package/dist/{build-DIsKQQT0.js → build-DANx9Hgg.js} +28 -13
  6. package/dist/{ci-D0nC_Ho7.js → ci-5qWl7lT3.js} +20 -4
  7. package/dist/client.d.ts +2 -2
  8. package/dist/{contract-DMrpbaLJ.d.ts → contract-rP_eV9PK.d.ts} +1 -1
  9. package/dist/{deploy-0dlUvCt3.js → deploy-MQWLG2EA.js} +130 -15
  10. package/dist/email-DBTVhNB_.js +62 -0
  11. package/dist/{generate-C6eAL5eo.js → generate-BxYHdoAQ.js} +18 -17
  12. package/dist/generate-CQ07Hdpo.js +2 -0
  13. package/dist/index.d.ts +115 -4
  14. package/dist/index.js +141 -26
  15. package/dist/{init-tEMeys9M.js → init-jDEgYIXg.js} +5 -5
  16. package/dist/load-config-BYSxDVAl.js +27 -0
  17. package/dist/login-C7RKmmTe.js +12 -0
  18. package/dist/{logs-m00pzr_O.js → logs-DgjgAxj2.js} +3 -3
  19. package/dist/{manifest-DyoJ1MIL.js → manifest-Bp87Uz0a.js} +6 -0
  20. package/dist/{manifest-BQCAxATS.d.ts → manifest-DD_SGkS6.d.ts} +5 -0
  21. package/dist/me-BW7Paary.js +32 -0
  22. package/dist/{project-mW0vsN8A.js → project-CbYPtpPq.js} +16 -14
  23. package/dist/prompt-ChS-grYO.js +29 -0
  24. package/dist/schedules-dhU1yyFr.js +42 -0
  25. package/dist/server.d.ts +2 -2
  26. package/dist/telephony-protocol-DRgiYjqQ.d.ts +74 -0
  27. package/dist/telephony.d.ts +77 -0
  28. package/dist/telephony.js +607 -0
  29. package/dist/{variables-CP7Ce3Np.js → variables-Bwnr7oxf.js} +3 -28
  30. package/dist/vite.d.ts +1 -1
  31. package/dist/vite.js +458 -27
  32. package/dist/workflow-engine-MogejNfF.js +117 -0
  33. package/dist/{auth-DaVK0k5R.js → workspace-XVtM12ZC.js} +44 -4
  34. package/package.json +6 -1
  35. package/scaffold/.agents/skills/hitch-applets/SKILL.md +32 -16
  36. package/scaffold/.agents/skills/hitch-applets/references/email-tracking.md +49 -0
  37. package/scaffold/.agents/skills/hitch-applets/references/files.md +3 -3
  38. package/scaffold/.agents/skills/hitch-applets/references/manifest.md +21 -2
  39. package/scaffold/.agents/skills/hitch-applets/references/performance.md +1 -1
  40. package/scaffold/.agents/skills/hitch-applets/references/records.md +4 -3
  41. package/scaffold/.agents/skills/hitch-applets/references/schedules.md +7 -2
  42. package/scaffold/.agents/skills/hitch-applets/references/server.md +0 -3
  43. package/scaffold/.agents/skills/hitch-applets/references/workflows.md +11 -3
  44. package/scaffold/.agents/skills/hitch-cli/SKILL.md +24 -22
  45. package/scaffold/.agents/skills/hitch-data/SKILL.md +2 -2
  46. package/scaffold/.agents/skills/hitch-data/references/limits.md +10 -10
  47. package/scaffold/.agents/skills/hitch-theory/SKILL.md +150 -0
  48. package/scaffold/.github/workflows/hitch.yml +0 -6
  49. package/dist/build-B70gkDi5.js +0 -2
  50. package/dist/generate-D5rSXXa-.js +0 -2
  51. package/dist/load-config-BnxocLAQ.js +0 -56
@@ -1,9 +1,13 @@
1
1
  import { t as AnyWorkflowDefinition } from "../workflow-DurM6Fwx.js";
2
+ import { d as listEmailTrackersInputSchema, i as createEmailPixelInputSchema, p as markEmailTrackerSentBodySchema, t as TelephonyMessageReceivedEvent } from "../telephony-protocol-DRgiYjqQ.js";
2
3
  import { z } from "zod";
3
4
  //#region src/server/entry.d.ts
4
5
  declare function createWorkerEntry(server: {
5
6
  router: object;
6
7
  workflows?: Record<string, AnyWorkflowDefinition>;
8
+ telephony?: {
9
+ onMessageReceived?: (event: TelephonyMessageReceivedEvent) => void | Promise<void>;
10
+ };
7
11
  }, options: {
8
12
  code: string;
9
13
  }): {
@@ -76,4 +80,13 @@ type ErrorOutcome = {
76
80
  declare const workflowOutcomeSchema: z.ZodType<StepOutcome | SleepOutcome | StepErrorOutcome | DoneOutcome | ErrorOutcome>;
77
81
  type WorkflowOutcome = z.infer<typeof workflowOutcomeSchema>;
78
82
  //#endregion
79
- export { type JournalEntry, type WorkflowInvocationRequest, type WorkflowOutcome, createWorkerEntry, journalEntrySchema, workflowInvocationRequestSchema, workflowOutcomeSchema };
83
+ //#region src/server/workflow-engine.d.ts
84
+ type Engine = {
85
+ invoke<T>(name: string, fn: () => Promise<T>): Promise<T>;
86
+ sleepUntil(name: string, until: Date): Promise<void>;
87
+ sleep(name: string, milliseconds: number): Promise<void>;
88
+ };
89
+ declare class WorkflowFailedError extends Error {}
90
+ declare function runWorkflowJournal(engine: Engine, invoke: (journal: JournalEntry[]) => Promise<WorkflowOutcome>): Promise<unknown>;
91
+ //#endregion
92
+ export { type Engine, type JournalEntry, WorkflowFailedError, type WorkflowInvocationRequest, type WorkflowOutcome, createEmailPixelInputSchema, createWorkerEntry, journalEntrySchema, listEmailTrackersInputSchema, markEmailTrackerSentBodySchema, runWorkflowJournal, workflowInvocationRequestSchema, workflowOutcomeSchema };
@@ -1,80 +1,16 @@
1
+ import { c as listEmailTrackersInputSchema, t as createEmailPixelInputSchema, u as markEmailTrackerSentBodySchema } from "../email-DBTVhNB_.js";
2
+ import { a as workflowOutcomeSchema, c as HITCH_SESSION_HEADER, i as workflowInvocationRequestSchema, l as HITCH_USER_HEADER, n as runWorkflowJournal, o as HITCH_APPLET_HEADER, r as journalEntrySchema, s as HITCH_APPLET_TOKEN_HEADER, t as WorkflowFailedError } from "../workflow-engine-MogejNfF.js";
1
3
  import { o as runWithRequestContext, r as toMilliseconds, t as MAX_DURATION_MS } from "../workflow-CTo2kZF1.js";
2
4
  import { z } from "zod";
3
5
  import { RPCHandler } from "@orpc/server/fetch";
4
- //#region src/headers.ts
5
- const HITCH_USER_HEADER = "x-hitch-user";
6
- const HITCH_SESSION_HEADER = "x-hitch-session";
7
- const HITCH_APPLET_HEADER = "x-hitch-applet";
8
- const HITCH_APPLET_TOKEN_HEADER = "x-hitch-applet-token";
9
- //#endregion
10
- //#region src/server/workflow-protocol.ts
11
- const journalEntrySchema = z.discriminatedUnion("kind", [
12
- z.object({
13
- kind: z.literal("step"),
14
- id: z.string(),
15
- name: z.string(),
16
- result: z.unknown()
17
- }),
18
- z.object({
19
- kind: z.literal("sleep"),
20
- id: z.string(),
21
- name: z.string(),
22
- until: z.number()
23
- }),
24
- z.object({
25
- kind: z.literal("attempt"),
26
- id: z.string(),
27
- name: z.string(),
28
- error: z.string()
29
- }),
30
- z.object({
31
- kind: z.literal("failed"),
32
- id: z.string(),
33
- name: z.string(),
34
- error: z.string()
35
- })
36
- ]);
37
- const workflowInvocationRequestSchema = z.object({
38
- payload: z.unknown(),
39
- journal: z.array(journalEntrySchema)
6
+ //#region src/server/telephony-protocol.ts
7
+ const telephonyMessageReceivedEventSchema = z.object({
8
+ eventId: z.string(),
9
+ type: z.literal("message.received"),
10
+ messageId: z.string(),
11
+ numberId: z.string(),
12
+ occurredAt: z.iso.datetime()
40
13
  });
41
- const workflowOutcomeSchema = z.discriminatedUnion("kind", [
42
- z.object({
43
- kind: z.literal("step"),
44
- id: z.string(),
45
- name: z.string(),
46
- result: z.unknown()
47
- }),
48
- z.object({
49
- kind: z.literal("sleep"),
50
- id: z.string(),
51
- name: z.string(),
52
- until: z.number()
53
- }),
54
- z.object({
55
- kind: z.literal("step_error"),
56
- id: z.string(),
57
- name: z.string(),
58
- message: z.string(),
59
- retries: z.object({
60
- limit: z.number(),
61
- delayMs: z.number(),
62
- backoff: z.enum([
63
- "constant",
64
- "linear",
65
- "exponential"
66
- ])
67
- })
68
- }),
69
- z.object({
70
- kind: z.literal("done"),
71
- result: z.unknown()
72
- }),
73
- z.object({
74
- kind: z.literal("error"),
75
- message: z.string()
76
- })
77
- ]);
78
14
  //#endregion
79
15
  //#region src/server/workflow-run.ts
80
16
  const MAX_RESULT_SIZE = 1048576;
@@ -209,6 +145,19 @@ function createWorkerEntry(server, options) {
209
145
  }
210
146
  }, async () => {
211
147
  const pathname = new URL(request.url).pathname;
148
+ if (pathname === "/api/__hitch/telephony/message-received" && request.method === "POST") {
149
+ if (identity.kind !== "applet") return jsonError(403, "applet_identity_required");
150
+ const parsed = await request.json().then((body) => telephonyMessageReceivedEventSchema.safeParse(body)).catch(() => void 0);
151
+ if (!parsed?.success) return jsonError(400, "malformed_request");
152
+ const onMessageReceived = server.telephony?.onMessageReceived;
153
+ if (!onMessageReceived) return jsonError(404, "no_telephony_handler");
154
+ try {
155
+ await onMessageReceived(parsed.data);
156
+ return Response.json({ ok: true });
157
+ } catch (error) {
158
+ return jsonError(500, error instanceof Error ? error.message : String(error));
159
+ }
160
+ }
212
161
  if (pathname.startsWith(WORKFLOW_PATH_PREFIX)) {
213
162
  const parsed = await request.json().then((body) => workflowInvocationRequestSchema.safeParse(body)).catch(() => void 0);
214
163
  if (parsed === void 0 || !parsed.success) return jsonError(400, "malformed_request");
@@ -229,4 +178,4 @@ function createWorkerEntry(server, options) {
229
178
  } };
230
179
  }
231
180
  //#endregion
232
- export { createWorkerEntry, journalEntrySchema, workflowInvocationRequestSchema, workflowOutcomeSchema };
181
+ export { WorkflowFailedError, createEmailPixelInputSchema, createWorkerEntry, journalEntrySchema, listEmailTrackersInputSchema, markEmailTrackerSentBodySchema, runWorkflowJournal, workflowInvocationRequestSchema, workflowOutcomeSchema };
package/dist/applet.js CHANGED
@@ -2,17 +2,22 @@
2
2
  import { parseArgs } from "node:util";
3
3
  //#region bin/applet.ts
4
4
  const usage = `Usage:
5
+ applet login [--no-browser]
5
6
  applet init <org> [--no-commit | --no-git] [--no-install]
6
7
  applet bootstrap [--no-install]
7
8
  applet new <code> [--client-only | --server-only] [--no-install]
8
- applet generate [--changeset <id>]
9
- applet build [--changeset <id>]
10
- applet deploy [--changeset <id>]
9
+ applet generate
10
+ applet build
11
+ applet deploy
11
12
  applet logs [code] [--since <duration>] [--limit <count>]
12
13
  [--search <text>] [--level <level>] [--filter <json>] [--run <id>] [--live]
13
14
  applet variables set <KEY> [value] [--secret] [-y | --yes]
14
15
  applet variables list
15
16
  applet variables delete <KEY> [-y | --yes]
17
+ applet schedules run <code> [-y | --yes]
18
+
19
+ Login options:
20
+ --no-browser Print the sign-in URL without opening a browser.
16
21
 
17
22
  Init options:
18
23
  --no-commit Do not create the initial commit
@@ -27,15 +32,14 @@ New options:
27
32
  Bootstrap options:
28
33
  --no-install Do not run pnpm install
29
34
 
30
- Generate options:
31
- --changeset <id> Generate against the schema this pending changeset
32
- would produce. The build honours HITCH_CHANGESET_ID.
35
+ Schema changesets:
36
+ Set requiresChangeset in applet.manifest.ts to generate, build, and deploy
37
+ against a pending changeset. The release goes live when the changeset
38
+ applies in Hitch. After it applies, the field uses the live schema.
33
39
 
34
- Deploy options:
35
- --changeset <id> Deploy against a pending schema changeset. The build
36
- generates against its schema and the release promotes
37
- when the changeset applies. Manifest mismatches warn.
38
- Run applet deploy in the workspace root to deploy every applet.
40
+ Run applet deploy in the workspace root to deploy every applet. In GitHub
41
+ Actions it skips an applet when neither its folder nor any file outside
42
+ applets/ changed since its current CI release.
39
43
 
40
44
  Logs options:
41
45
  --since <duration> Show logs from this period. Default: 1h. Maximum: 7d.
@@ -52,6 +56,11 @@ Variables options:
52
56
  --secret Store the value as a secret.
53
57
  -y, --yes Skip the live applet confirmation.
54
58
 
59
+ Schedules options:
60
+ -y, --yes Skip the schedule confirmation.
61
+
62
+ Login: applet login selects the organization from hitch.config.ts and shares
63
+ credentials with hitch login.
55
64
  CI: set HITCH_API_KEY (and HITCH_API_URL for a non-default instance).
56
65
  GitHub Actions: generate, build, and deploy use the job's OIDC token instead of
57
66
  a key. Grant the job id-token: write.`;
@@ -59,13 +68,13 @@ async function main() {
59
68
  const { positionals, values } = parseArgs({
60
69
  allowPositionals: true,
61
70
  options: {
62
- changeset: { type: "string" },
63
71
  "client-only": { type: "boolean" },
64
72
  filter: { type: "string" },
65
73
  run: { type: "string" },
66
74
  level: { type: "string" },
67
75
  limit: { type: "string" },
68
76
  live: { type: "boolean" },
77
+ "no-browser": { type: "boolean" },
69
78
  "no-commit": { type: "boolean" },
70
79
  "no-git": { type: "boolean" },
71
80
  "no-install": { type: "boolean" },
@@ -80,36 +89,41 @@ async function main() {
80
89
  },
81
90
  strict: true
82
91
  });
92
+ if (positionals.length === 1 && positionals[0] === "login" && Object.keys(values).every((key) => key === "no-browser")) {
93
+ await (await import("./login-C7RKmmTe.js")).runLogin({ noBrowser: values["no-browser"] });
94
+ return;
95
+ }
96
+ if (values["no-browser"] !== void 0) throw new Error(usage);
83
97
  const noGitFlags = values["no-commit"] === void 0 && values["no-git"] === void 0;
84
98
  const noVariableFlags = values.secret === void 0 && values.yes === void 0;
85
99
  const noLogFlags = values.run === void 0 && values.filter === void 0 && values.level === void 0 && values.limit === void 0 && values.live === void 0 && values.search === void 0 && values.since === void 0;
86
100
  const noModeFlags = values["client-only"] === void 0 && values["server-only"] === void 0;
87
- if (positionals.length === 1 && positionals[0] === "bootstrap" && values.changeset === void 0 && noModeFlags && noGitFlags && noVariableFlags && noLogFlags) {
88
- await (await import("./init-tEMeys9M.js")).runBootstrap({ install: values["no-install"] !== true });
101
+ if (positionals.length === 1 && positionals[0] === "bootstrap" && noModeFlags && noGitFlags && noVariableFlags && noLogFlags) {
102
+ await (await import("./init-jDEgYIXg.js")).runBootstrap({ install: values["no-install"] !== true });
89
103
  return;
90
104
  }
91
105
  if (positionals.length === 1 && positionals[0] === "generate" && values["no-install"] === void 0 && noModeFlags && noGitFlags && noVariableFlags && noLogFlags) {
92
- await (await import("./generate-D5rSXXa-.js")).runGenerate({ changesetId: values.changeset });
106
+ await (await import("./generate-CQ07Hdpo.js")).runGenerate({});
93
107
  return;
94
108
  }
95
109
  if (positionals.length === 1 && positionals[0] === "build" && values["no-install"] === void 0 && noModeFlags && noGitFlags && noVariableFlags && noLogFlags) {
96
- await (await import("./build-B70gkDi5.js")).runBuild({ changesetId: values.changeset });
110
+ await (await import("./build-CFCKKMn7.js")).runBuild();
97
111
  return;
98
112
  }
99
113
  if (positionals.length === 1 && positionals[0] === "deploy" && values["no-install"] === void 0 && noModeFlags && noGitFlags && noVariableFlags && noLogFlags) {
100
- await (await import("./deploy-0dlUvCt3.js")).runDeploy({ changesetId: values.changeset });
114
+ await (await import("./deploy-MQWLG2EA.js")).runDeploy({});
101
115
  return;
102
116
  }
103
- if (positionals.length === 2 && positionals[0] === "init" && values.changeset === void 0 && noModeFlags && noVariableFlags && noLogFlags) {
104
- await (await import("./init-tEMeys9M.js")).runInit(positionals[1], {
117
+ if (positionals.length === 2 && positionals[0] === "init" && noModeFlags && noVariableFlags && noLogFlags) {
118
+ await (await import("./init-jDEgYIXg.js")).runInit(positionals[1], {
105
119
  commit: values["no-commit"] !== true,
106
120
  git: values["no-git"] !== true,
107
121
  install: values["no-install"] !== true
108
122
  });
109
123
  return;
110
124
  }
111
- if (positionals.length === 2 && positionals[0] === "new" && values.changeset === void 0 && noGitFlags && noVariableFlags && noLogFlags) {
112
- await (await import("./init-tEMeys9M.js")).runNew({
125
+ if (positionals.length === 2 && positionals[0] === "new" && noGitFlags && noVariableFlags && noLogFlags) {
126
+ await (await import("./init-jDEgYIXg.js")).runNew({
113
127
  code: positionals[1],
114
128
  clientOnly: values["client-only"],
115
129
  install: values["no-install"] !== true,
@@ -117,8 +131,8 @@ async function main() {
117
131
  });
118
132
  return;
119
133
  }
120
- if ((positionals.length === 1 || positionals.length === 2) && positionals[0] === "logs" && values.changeset === void 0 && values["no-install"] === void 0 && noModeFlags && noGitFlags && noVariableFlags) {
121
- await (await import("./logs-m00pzr_O.js")).runLogs({
134
+ if ((positionals.length === 1 || positionals.length === 2) && positionals[0] === "logs" && values["no-install"] === void 0 && noModeFlags && noGitFlags && noVariableFlags) {
135
+ await (await import("./logs-DgjgAxj2.js")).runLogs({
122
136
  code: positionals[1],
123
137
  filter: values.filter,
124
138
  run: values.run,
@@ -130,8 +144,15 @@ async function main() {
130
144
  });
131
145
  return;
132
146
  }
133
- if ((positionals.length === 3 || positionals.length === 4) && positionals[0] === "variables" && values.changeset === void 0 && positionals[1] === "set" && values["no-install"] === void 0 && noModeFlags && noGitFlags && noLogFlags) {
134
- await (await import("./variables-CP7Ce3Np.js")).runVariablesSet({
147
+ if (positionals.length === 3 && positionals[0] === "schedules" && positionals[1] === "run" && values["no-install"] === void 0 && noModeFlags && noGitFlags && noLogFlags && values.secret === void 0) {
148
+ await (await import("./schedules-dhU1yyFr.js")).runSchedule({
149
+ code: positionals[2],
150
+ yes: values.yes
151
+ });
152
+ return;
153
+ }
154
+ if ((positionals.length === 3 || positionals.length === 4) && positionals[0] === "variables" && positionals[1] === "set" && values["no-install"] === void 0 && noModeFlags && noGitFlags && noLogFlags) {
155
+ await (await import("./variables-Bwnr7oxf.js")).runVariablesSet({
135
156
  key: positionals[2],
136
157
  secret: values.secret,
137
158
  value: positionals[3],
@@ -139,12 +160,12 @@ async function main() {
139
160
  });
140
161
  return;
141
162
  }
142
- if (positionals.length === 2 && positionals[0] === "variables" && values.changeset === void 0 && positionals[1] === "list" && values["no-install"] === void 0 && noModeFlags && noGitFlags && noVariableFlags && noLogFlags) {
143
- await (await import("./variables-CP7Ce3Np.js")).runVariablesList({});
163
+ if (positionals.length === 2 && positionals[0] === "variables" && positionals[1] === "list" && values["no-install"] === void 0 && noModeFlags && noGitFlags && noVariableFlags && noLogFlags) {
164
+ await (await import("./variables-Bwnr7oxf.js")).runVariablesList({});
144
165
  return;
145
166
  }
146
- if (positionals.length === 3 && positionals[0] === "variables" && values.changeset === void 0 && positionals[1] === "delete" && values["no-install"] === void 0 && noModeFlags && noGitFlags && values.secret === void 0 && noLogFlags) {
147
- await (await import("./variables-CP7Ce3Np.js")).runVariablesDelete({
167
+ if (positionals.length === 3 && positionals[0] === "variables" && positionals[1] === "delete" && values["no-install"] === void 0 && noModeFlags && noGitFlags && values.secret === void 0 && noLogFlags) {
168
+ await (await import("./variables-Bwnr7oxf.js")).runVariablesDelete({
148
169
  key: positionals[2],
149
170
  yes: values.yes
150
171
  });
@@ -0,0 +1,2 @@
1
+ import { i as runBuild } from "./build-DANx9Hgg.js";
2
+ export { runBuild };
@@ -1,7 +1,8 @@
1
- import { r as prepareAppletProject } from "./project-mW0vsN8A.js";
1
+ import { i as prepareAppletProjects, r as prepareAppletProject } from "./project-CbYPtpPq.js";
2
2
  import { rm } from "node:fs/promises";
3
3
  import { resolve } from "node:path";
4
4
  import { spawn } from "node:child_process";
5
+ import { createInterface } from "node:readline";
5
6
  //#region src/build.ts
6
7
  function isMissingFile(error) {
7
8
  return error instanceof Error && "code" in error && error.code === "ENOENT";
@@ -29,13 +30,19 @@ async function runPnpm(args, cwd) {
29
30
  });
30
31
  });
31
32
  }
32
- async function buildApplet(appletRoot, changesetId, outDir = ".hitch/dist") {
33
- const { workspaceRoot } = await prepareAppletProject(appletRoot);
33
+ async function buildApplet(appletRoot, outDir = ".hitch/dist") {
34
+ await runPnpm(["install"], await prepareAppletProject(appletRoot));
35
+ await buildPreparedApplet(appletRoot, outDir);
36
+ }
37
+ async function prepareWorkspaceBuild(workspaceRoot, appletRoots) {
38
+ await prepareAppletProjects(workspaceRoot, appletRoots);
39
+ await runPnpm(["install"], workspaceRoot);
40
+ }
41
+ async function buildPreparedApplet(appletRoot, outDir = ".hitch/dist", logPrefix = "") {
34
42
  await rm(resolve(appletRoot, ".hitch"), {
35
43
  force: true,
36
44
  recursive: true
37
45
  });
38
- await runPnpm(["install"], workspaceRoot);
39
46
  await new Promise((resolveBuild, rejectBuild) => {
40
47
  let finished = false;
41
48
  const child = spawn("pnpm", [
@@ -46,13 +53,21 @@ async function buildApplet(appletRoot, changesetId, outDir = ".hitch/dist") {
46
53
  outDir
47
54
  ], {
48
55
  cwd: appletRoot,
49
- stdio: "inherit",
50
- shell: process.platform === "win32",
51
- env: {
52
- ...process.env,
53
- ...changesetId === void 0 ? {} : { HITCH_CHANGESET_ID: changesetId }
54
- }
56
+ stdio: logPrefix === "" ? "inherit" : [
57
+ "ignore",
58
+ "pipe",
59
+ "pipe"
60
+ ],
61
+ shell: process.platform === "win32"
55
62
  });
63
+ if (child.stdout) createInterface({
64
+ input: child.stdout,
65
+ crlfDelay: Infinity
66
+ }).on("line", (line) => console.log(`${logPrefix}${line}`));
67
+ if (child.stderr) createInterface({
68
+ input: child.stderr,
69
+ crlfDelay: Infinity
70
+ }).on("line", (line) => console.error(`${logPrefix}${line}`));
56
71
  child.once("error", (error) => {
57
72
  finished = true;
58
73
  rejectBuild(isMissingFile(error) ? /* @__PURE__ */ new Error("pnpm was not found. Install pnpm, then run applet build again.") : error);
@@ -68,8 +83,8 @@ async function buildApplet(appletRoot, changesetId, outDir = ".hitch/dist") {
68
83
  });
69
84
  });
70
85
  }
71
- async function runBuild(options = {}) {
72
- await buildApplet(process.cwd(), options.changesetId);
86
+ async function runBuild() {
87
+ await buildApplet(process.cwd());
73
88
  }
74
89
  //#endregion
75
- export { runBuild as n, buildApplet as t };
90
+ export { runBuild as i, buildPreparedApplet as n, prepareWorkspaceBuild as r, buildApplet as t };
@@ -1,4 +1,4 @@
1
- import { i as resolveInstanceUrl } from "./auth-DaVK0k5R.js";
1
+ import { d as resolveInstanceUrl } from "./workspace-XVtM12ZC.js";
2
2
  //#region src/ci.ts
3
3
  function githubActionsContext(env = process.env) {
4
4
  if (env.GITHUB_ACTIONS !== "true" || env.HITCH_API_KEY) return void 0;
@@ -34,9 +34,10 @@ async function fetchWithGitHubActions(context, options, request) {
34
34
  const instanceUrl = await resolveInstanceUrl(void 0, options.store);
35
35
  return request(instanceUrl, await githubActionsToken(context, instanceUrl, options.fetch ?? globalThis.fetch));
36
36
  }
37
- async function loadGitHubActionsBuildInputs(context, options) {
37
+ async function loadGitHubActionsBuildInputs(context, options, requiresChangeset) {
38
38
  const fetcher = options.fetch ?? globalThis.fetch;
39
- const response = await fetchWithGitHubActions(context, options, (instanceUrl, accessToken) => fetcher(`${instanceUrl}/api/ci/v1/build-inputs`, {
39
+ const query = requiresChangeset === void 0 ? "" : `?changeset_id=${encodeURIComponent(requiresChangeset)}`;
40
+ const response = await fetchWithGitHubActions(context, options, (instanceUrl, accessToken) => fetcher(`${instanceUrl}/api/ci/v1/build-inputs${query}`, {
40
41
  method: "POST",
41
42
  headers: { Authorization: `Bearer ${accessToken}` }
42
43
  }));
@@ -48,5 +49,20 @@ async function loadGitHubActionsBuildInputs(context, options) {
48
49
  theme: body.theme
49
50
  };
50
51
  }
52
+ async function loadGitHubActionsSources(context, options) {
53
+ const fetcher = options.fetch ?? globalThis.fetch;
54
+ const response = await fetchWithGitHubActions(context, options, (instanceUrl, accessToken) => fetcher(`${instanceUrl}/api/ci/v1/applets`, { headers: { Authorization: `Bearer ${accessToken}` } }));
55
+ if (!response.ok) throw await hitchResponseError(response, "Failed to load the CI applets");
56
+ const body = await response.json();
57
+ if (body === null || typeof body !== "object" || !("data" in body) || !Array.isArray(body.data)) throw new Error("The CI applets response must contain a data array.");
58
+ return body.data.map((source) => {
59
+ if (source === null || typeof source !== "object" || !("code" in source) || typeof source.code !== "string" || !("source_directory" in source) || typeof source.source_directory !== "string" || !("git_sha" in source) || source.git_sha !== null && typeof source.git_sha !== "string") throw new Error("Each CI applet must contain code, source_directory, and git_sha fields.");
60
+ return {
61
+ code: source.code,
62
+ sourceDirectory: source.source_directory,
63
+ gitSha: source.git_sha
64
+ };
65
+ });
66
+ }
51
67
  //#endregion
52
- export { loadGitHubActionsBuildInputs as i, githubActionsContext as n, hitchResponseError as r, fetchWithGitHubActions as t };
68
+ export { loadGitHubActionsSources as a, loadGitHubActionsBuildInputs as i, githubActionsContext as n, hitchResponseError as r, fetchWithGitHubActions as t };
package/dist/client.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { f as RegisteredContract, m as RegisteredRouter, p as RegisteredManifest } from "./manifest-BQCAxATS.js";
2
- import { t as NarrowContract } from "./contract-DMrpbaLJ.js";
1
+ import { f as RegisteredContract, m as RegisteredRouter, p as RegisteredManifest } from "./manifest-DD_SGkS6.js";
2
+ import { t as NarrowContract } from "./contract-rP_eV9PK.js";
3
3
  import { t as WithBatchRecorder } from "./batch-Qa4B4-Yp.js";
4
4
  import { o as WithFilesUpload } from "./upload-YbRKEqR6.js";
5
5
  import { RouterClient } from "@orpc/server";
@@ -1,4 +1,4 @@
1
- import { n as AppletManifest } from "./manifest-BQCAxATS.js";
1
+ import { n as AppletManifest } from "./manifest-DD_SGkS6.js";
2
2
  import { AnyContractRouter } from "@orpc/contract";
3
3
  //#region src/hitch/contract.d.ts
4
4
  type ManifestObjectFor<Manifest, Code> = Manifest extends {
@@ -1,7 +1,6 @@
1
- import { t as fetchWithOrgAuth } from "./auth-DaVK0k5R.js";
2
- import { n as githubActionsContext, r as hitchResponseError, t as fetchWithGitHubActions } from "./ci-D0nC_Ho7.js";
3
- import { r as readWorkspaceConfig } from "./load-config-BnxocLAQ.js";
4
- import { t as buildApplet } from "./build-DIsKQQT0.js";
1
+ import { i as readWorkspaceConfig, n as fetchWithOrgAuth } from "./workspace-XVtM12ZC.js";
2
+ import { a as loadGitHubActionsSources, n as githubActionsContext, r as hitchResponseError, t as fetchWithGitHubActions } from "./ci-5qWl7lT3.js";
3
+ import { n as buildPreparedApplet, r as prepareWorkspaceBuild, t as buildApplet } from "./build-DANx9Hgg.js";
5
4
  import { createRequire } from "node:module";
6
5
  import { existsSync } from "node:fs";
7
6
  import { readFile, readdir } from "node:fs/promises";
@@ -2314,6 +2313,12 @@ var import_tar_stream = /* @__PURE__ */ __toESM((/* @__PURE__ */ __commonJSMin((
2314
2313
  exports.pack = require_pack();
2315
2314
  })))(), 1);
2316
2315
  const execFileAsync = promisify(execFile);
2316
+ var WorkspaceDeployError = class extends Error {
2317
+ constructor(failedApplets) {
2318
+ super(`Failed to deploy: ${failedApplets.join(", ")}. See the applet logs above.`);
2319
+ this.name = "WorkspaceDeployError";
2320
+ }
2321
+ };
2317
2322
  function isErrno(error, code) {
2318
2323
  return error instanceof Error && "code" in error && error.code === code;
2319
2324
  }
@@ -2401,7 +2406,6 @@ async function deployWithOrgAuth(appletRoot, distDir, manifest, options) {
2401
2406
  const body = releaseForm(manifest, bundle);
2402
2407
  body.set("gitDirty", String(git.gitDirty));
2403
2408
  if (git.gitSha !== void 0) body.set("gitSha", git.gitSha);
2404
- if (options.changesetId !== void 0) body.set("changeset_id", options.changesetId);
2405
2409
  return fetcher(`${instanceUrl}/api/v1/applets/${manifest.code}/releases`, {
2406
2410
  method: "POST",
2407
2411
  headers: { Authorization: `Bearer ${accessToken}` },
@@ -2422,21 +2426,21 @@ async function deployWithGitHubActions(context, appletRoot, distDir, manifest, o
2422
2426
  });
2423
2427
  });
2424
2428
  }
2425
- async function deployApplet(appletRoot, options) {
2429
+ async function deployApplet(appletRoot, options, logPrefix = "") {
2426
2430
  const github = githubActionsContext(options.store?.env ?? process.env);
2427
- if (github !== void 0 && options.changesetId !== void 0) throw new Error("A changeset deploy is not available in GitHub Actions.");
2428
- if (options.dist === void 0) await buildApplet(appletRoot, options.changesetId);
2431
+ if (options.dist === void 0) await buildApplet(appletRoot);
2429
2432
  const distDir = resolve(appletRoot, options.dist ?? ".hitch/dist");
2430
2433
  const manifest = await readManifest(distDir);
2431
2434
  const response = github === void 0 ? await deployWithOrgAuth(appletRoot, distDir, manifest, options) : await deployWithGitHubActions(github, appletRoot, distDir, manifest, options);
2432
2435
  if (!response.ok) throw await hitchResponseError(response, "Deploy failed");
2433
2436
  const result = await response.json();
2434
- console.log(`Deployed release ${result.data.id} (${result.promoted ? "promoted" : "not promoted"}).`);
2437
+ console.log(`${logPrefix}Deployed release ${result.data.id} (${result.promoted ? "promoted" : "not promoted"}).`);
2438
+ if (result.changeset_id !== void 0) console.log(`${logPrefix}The release goes live when changeset ${result.changeset_id} applies.`);
2435
2439
  if (result.warnings?.length) {
2436
- for (const warning of result.warnings) console.warn(`Warning: ${warning.message}`);
2437
- console.warn(`Changeset ${result.changeset_id} cannot apply until the applet and the schema agree.`);
2440
+ for (const warning of result.warnings) console.warn(`${logPrefix}Warning: ${warning.message}`);
2441
+ console.warn(`${logPrefix}Changeset ${result.changeset_id} cannot apply until the applet and the schema agree.`);
2438
2442
  }
2439
- if (result.unset_variables?.length) console.warn(`Warning: declared variables not set: ${result.unset_variables.join(", ")}. Set them with applet variables set.`);
2443
+ if (result.unset_variables?.length) console.warn(`${logPrefix}Warning: declared variables not set: ${result.unset_variables.join(", ")}. Set them with applet variables set.`);
2440
2444
  return result;
2441
2445
  }
2442
2446
  async function workspaceAppletRoots(workspaceRoot) {
@@ -2448,17 +2452,128 @@ async function workspaceAppletRoots(workspaceRoot) {
2448
2452
  if (roots.length === 0) throw new Error(`No applets were found under ${appletsDir}.`);
2449
2453
  return roots;
2450
2454
  }
2455
+ async function gitChanged(topLevel, base, pathspecs, env) {
2456
+ try {
2457
+ await execFileAsync("git", [
2458
+ "diff",
2459
+ "--quiet",
2460
+ base,
2461
+ "HEAD",
2462
+ "--",
2463
+ ...pathspecs
2464
+ ], {
2465
+ cwd: topLevel,
2466
+ env
2467
+ });
2468
+ return false;
2469
+ } catch (error) {
2470
+ if (error instanceof Error && "code" in error && error.code === 1) return true;
2471
+ throw error;
2472
+ }
2473
+ }
2474
+ async function hasCommit(topLevel, sha, env) {
2475
+ const git = (args) => execFileAsync("git", args, {
2476
+ cwd: topLevel,
2477
+ env
2478
+ });
2479
+ return git([
2480
+ "cat-file",
2481
+ "-e",
2482
+ `${sha}^{commit}`
2483
+ ]).catch(() => git([
2484
+ "fetch",
2485
+ "--depth=1",
2486
+ "origin",
2487
+ sha
2488
+ ])).then(() => true, () => false);
2489
+ }
2490
+ async function ciDeployDecisions(github, workspaceRoot, appletRoots, options) {
2491
+ const env = gitEnvironment();
2492
+ const [sources, { stdout: topLevelOutput }, directories] = await Promise.all([
2493
+ loadGitHubActionsSources(github, options),
2494
+ execFileAsync("git", ["rev-parse", "--show-toplevel"], {
2495
+ cwd: workspaceRoot,
2496
+ env
2497
+ }),
2498
+ Promise.all(appletRoots.map((appletRoot) => repositoryDirectory(appletRoot)))
2499
+ ]);
2500
+ const topLevel = topLevelOutput.trim();
2501
+ const appletsDirectory = relative(topLevel, resolve(workspaceRoot, "applets")).split(sep).join("/");
2502
+ const baseByDirectory = new Map(sources.map((source) => [source.sourceDirectory, source.gitSha]));
2503
+ const bases = new Set(directories.flatMap((directory) => baseByDirectory.get(directory) ?? []));
2504
+ const sharedChanges = /* @__PURE__ */ new Map();
2505
+ for (const base of bases) sharedChanges.set(base, await hasCommit(topLevel, base, env) ? await gitChanged(topLevel, base, [`:(exclude)${appletsDirectory}`], env) : void 0);
2506
+ const decisions = await Promise.all(appletRoots.map(async (appletRoot, index) => {
2507
+ const directory = directories[index];
2508
+ const base = baseByDirectory.get(directory) ?? null;
2509
+ if (base === null) return [appletRoot, {
2510
+ deploy: true,
2511
+ reason: "no CI release to compare"
2512
+ }];
2513
+ const short = base.slice(0, 7);
2514
+ const sharedChanged = sharedChanges.get(base);
2515
+ if (sharedChanged === void 0) return [appletRoot, {
2516
+ deploy: true,
2517
+ reason: `commit ${short} is not in the repository`
2518
+ }];
2519
+ if (await gitChanged(topLevel, base, [directory], env)) return [appletRoot, {
2520
+ deploy: true,
2521
+ reason: `${directory} changed since ${short}`
2522
+ }];
2523
+ if (sharedChanged) return [appletRoot, {
2524
+ deploy: true,
2525
+ reason: `files outside ${appletsDirectory} changed since ${short}`
2526
+ }];
2527
+ return [appletRoot, {
2528
+ deploy: false,
2529
+ reason: `no changes since ${short}`
2530
+ }];
2531
+ }));
2532
+ return new Map(decisions);
2533
+ }
2451
2534
  async function runDeploy(options) {
2452
2535
  const root = process.cwd();
2536
+ const github = githubActionsContext(options.store?.env ?? process.env);
2453
2537
  if (existsSync(resolve(root, "applet.manifest.ts"))) {
2454
2538
  await deployApplet(root, options);
2455
2539
  return;
2456
2540
  }
2457
2541
  if (readWorkspaceConfig(root) === void 0) throw new Error("Run applet deploy in an applet folder or in the workspace root.");
2458
- for (const appletRoot of await workspaceAppletRoots(root)) {
2459
- console.log(`Deploying ${relative(root, appletRoot)}...`);
2460
- await deployApplet(appletRoot, options);
2542
+ const appletRoots = await workspaceAppletRoots(root);
2543
+ const decisions = github === void 0 ? void 0 : await ciDeployDecisions(github, root, appletRoots, options);
2544
+ const selectedRoots = appletRoots.filter((appletRoot) => {
2545
+ const name = relative(root, appletRoot);
2546
+ const decision = decisions?.get(appletRoot);
2547
+ if (decision === void 0) console.log(`Deploying ${name}...`);
2548
+ else if (decision.deploy) console.log(`Deploying ${name}: ${decision.reason}.`);
2549
+ else {
2550
+ console.log(`Skipping ${name}: ${decision.reason}.`);
2551
+ return false;
2552
+ }
2553
+ return true;
2554
+ });
2555
+ if (selectedRoots.length === 0) return;
2556
+ if (options.dist === void 0) await prepareWorkspaceBuild(root, appletRoots);
2557
+ const interactive = github === void 0 && (options.stdinIsTTY ?? process.stdin.isTTY === true);
2558
+ const deploy = async (appletRoot) => {
2559
+ const logPrefix = `[${relative(root, appletRoot)}] `;
2560
+ if (options.dist === void 0) await buildPreparedApplet(appletRoot, ".hitch/dist", interactive ? "" : logPrefix);
2561
+ await deployApplet(appletRoot, {
2562
+ ...options,
2563
+ dist: options.dist ?? ".hitch/dist"
2564
+ }, logPrefix);
2565
+ };
2566
+ if (interactive) {
2567
+ for (const appletRoot of selectedRoots) await deploy(appletRoot);
2568
+ return;
2461
2569
  }
2570
+ const failedApplets = (await Promise.allSettled(selectedRoots.map(deploy))).flatMap((result, index) => {
2571
+ if (result.status === "fulfilled") return [];
2572
+ const name = relative(root, selectedRoots[index]);
2573
+ console.error(`[${name}] ${result.reason instanceof Error ? result.reason.message : String(result.reason)}`);
2574
+ return [name];
2575
+ });
2576
+ if (failedApplets.length > 0) throw new WorkspaceDeployError(failedApplets);
2462
2577
  }
2463
2578
  //#endregion
2464
2579
  export { runDeploy };