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

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 (32) hide show
  1. package/dist/applet.js +39 -29
  2. package/dist/{build-DIsKQQT0.js → build-DauirTcY.js} +28 -13
  3. package/dist/build-DhNDp7ji.js +2 -0
  4. package/dist/{ci-D0nC_Ho7.js → ci-gPrwo6C0.js} +20 -4
  5. package/dist/client.d.ts +2 -2
  6. package/dist/{contract-DMrpbaLJ.d.ts → contract-DB9QQhD9.d.ts} +1 -1
  7. package/dist/{deploy-0dlUvCt3.js → deploy-CqrtNJ1x.js} +130 -15
  8. package/dist/{generate-C6eAL5eo.js → generate-BIifT6PK.js} +13 -13
  9. package/dist/generate-DSk8ihsC.js +2 -0
  10. package/dist/index.d.ts +3 -3
  11. package/dist/index.js +1 -1
  12. package/dist/{init-tEMeys9M.js → init-Wu6XmyVc.js} +5 -5
  13. package/dist/load-config-BknBkdRs.js +27 -0
  14. package/dist/login-iT9MRuaw.js +12 -0
  15. package/dist/{logs-m00pzr_O.js → logs-B-qZGXSK.js} +3 -3
  16. package/dist/{manifest-DyoJ1MIL.js → manifest-Bd04cWhu.js} +3 -0
  17. package/dist/{manifest-BQCAxATS.d.ts → manifest-C489nYUy.d.ts} +2 -0
  18. package/dist/{project-mW0vsN8A.js → project-C_rtpjp8.js} +12 -11
  19. package/dist/server.d.ts +2 -2
  20. package/dist/{variables-CP7Ce3Np.js → variables-DcwMeIau.js} +3 -3
  21. package/dist/vite.d.ts +1 -1
  22. package/dist/vite.js +4 -8
  23. package/dist/{auth-DaVK0k5R.js → workspace-B6lenAS5.js} +41 -4
  24. package/package.json +1 -1
  25. package/scaffold/.agents/skills/hitch-applets/SKILL.md +10 -0
  26. package/scaffold/.agents/skills/hitch-applets/references/manifest.md +17 -0
  27. package/scaffold/.agents/skills/hitch-cli/SKILL.md +5 -3
  28. package/scaffold/.agents/skills/hitch-data/SKILL.md +1 -1
  29. package/scaffold/.github/workflows/hitch.yml +0 -6
  30. package/dist/build-B70gkDi5.js +0 -2
  31. package/dist/generate-D5rSXXa-.js +0 -2
  32. package/dist/load-config-BnxocLAQ.js +0 -56
package/dist/applet.js CHANGED
@@ -2,18 +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]
16
17
 
18
+ Login options:
19
+ --no-browser Print the sign-in URL without opening a browser.
20
+
17
21
  Init options:
18
22
  --no-commit Do not create the initial commit
19
23
  --no-git Do not create a git repository
@@ -27,15 +31,14 @@ New options:
27
31
  Bootstrap options:
28
32
  --no-install Do not run pnpm install
29
33
 
30
- Generate options:
31
- --changeset <id> Generate against the schema this pending changeset
32
- would produce. The build honours HITCH_CHANGESET_ID.
34
+ Schema changesets:
35
+ Set requiresChangeset in applet.manifest.ts to generate, build, and deploy
36
+ against a pending changeset. The release goes live when the changeset
37
+ applies in Hitch. After it applies, the field uses the live schema.
33
38
 
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.
39
+ Run applet deploy in the workspace root to deploy every applet. In GitHub
40
+ Actions it skips an applet when neither its folder nor any file outside
41
+ applets/ changed since its current CI release.
39
42
 
40
43
  Logs options:
41
44
  --since <duration> Show logs from this period. Default: 1h. Maximum: 7d.
@@ -52,6 +55,8 @@ Variables options:
52
55
  --secret Store the value as a secret.
53
56
  -y, --yes Skip the live applet confirmation.
54
57
 
58
+ Login: applet login selects the organization from hitch.config.ts and shares
59
+ credentials with hitch login.
55
60
  CI: set HITCH_API_KEY (and HITCH_API_URL for a non-default instance).
56
61
  GitHub Actions: generate, build, and deploy use the job's OIDC token instead of
57
62
  a key. Grant the job id-token: write.`;
@@ -59,13 +64,13 @@ async function main() {
59
64
  const { positionals, values } = parseArgs({
60
65
  allowPositionals: true,
61
66
  options: {
62
- changeset: { type: "string" },
63
67
  "client-only": { type: "boolean" },
64
68
  filter: { type: "string" },
65
69
  run: { type: "string" },
66
70
  level: { type: "string" },
67
71
  limit: { type: "string" },
68
72
  live: { type: "boolean" },
73
+ "no-browser": { type: "boolean" },
69
74
  "no-commit": { type: "boolean" },
70
75
  "no-git": { type: "boolean" },
71
76
  "no-install": { type: "boolean" },
@@ -80,36 +85,41 @@ async function main() {
80
85
  },
81
86
  strict: true
82
87
  });
88
+ if (positionals.length === 1 && positionals[0] === "login" && Object.keys(values).every((key) => key === "no-browser")) {
89
+ await (await import("./login-iT9MRuaw.js")).runLogin({ noBrowser: values["no-browser"] });
90
+ return;
91
+ }
92
+ if (values["no-browser"] !== void 0) throw new Error(usage);
83
93
  const noGitFlags = values["no-commit"] === void 0 && values["no-git"] === void 0;
84
94
  const noVariableFlags = values.secret === void 0 && values.yes === void 0;
85
95
  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
96
  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 });
97
+ if (positionals.length === 1 && positionals[0] === "bootstrap" && noModeFlags && noGitFlags && noVariableFlags && noLogFlags) {
98
+ await (await import("./init-Wu6XmyVc.js")).runBootstrap({ install: values["no-install"] !== true });
89
99
  return;
90
100
  }
91
101
  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 });
102
+ await (await import("./generate-DSk8ihsC.js")).runGenerate({});
93
103
  return;
94
104
  }
95
105
  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 });
106
+ await (await import("./build-DhNDp7ji.js")).runBuild();
97
107
  return;
98
108
  }
99
109
  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 });
110
+ await (await import("./deploy-CqrtNJ1x.js")).runDeploy({});
101
111
  return;
102
112
  }
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], {
113
+ if (positionals.length === 2 && positionals[0] === "init" && noModeFlags && noVariableFlags && noLogFlags) {
114
+ await (await import("./init-Wu6XmyVc.js")).runInit(positionals[1], {
105
115
  commit: values["no-commit"] !== true,
106
116
  git: values["no-git"] !== true,
107
117
  install: values["no-install"] !== true
108
118
  });
109
119
  return;
110
120
  }
111
- if (positionals.length === 2 && positionals[0] === "new" && values.changeset === void 0 && noGitFlags && noVariableFlags && noLogFlags) {
112
- await (await import("./init-tEMeys9M.js")).runNew({
121
+ if (positionals.length === 2 && positionals[0] === "new" && noGitFlags && noVariableFlags && noLogFlags) {
122
+ await (await import("./init-Wu6XmyVc.js")).runNew({
113
123
  code: positionals[1],
114
124
  clientOnly: values["client-only"],
115
125
  install: values["no-install"] !== true,
@@ -117,8 +127,8 @@ async function main() {
117
127
  });
118
128
  return;
119
129
  }
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({
130
+ if ((positionals.length === 1 || positionals.length === 2) && positionals[0] === "logs" && values["no-install"] === void 0 && noModeFlags && noGitFlags && noVariableFlags) {
131
+ await (await import("./logs-B-qZGXSK.js")).runLogs({
122
132
  code: positionals[1],
123
133
  filter: values.filter,
124
134
  run: values.run,
@@ -130,8 +140,8 @@ async function main() {
130
140
  });
131
141
  return;
132
142
  }
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({
143
+ if ((positionals.length === 3 || positionals.length === 4) && positionals[0] === "variables" && positionals[1] === "set" && values["no-install"] === void 0 && noModeFlags && noGitFlags && noLogFlags) {
144
+ await (await import("./variables-DcwMeIau.js")).runVariablesSet({
135
145
  key: positionals[2],
136
146
  secret: values.secret,
137
147
  value: positionals[3],
@@ -139,12 +149,12 @@ async function main() {
139
149
  });
140
150
  return;
141
151
  }
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({});
152
+ if (positionals.length === 2 && positionals[0] === "variables" && positionals[1] === "list" && values["no-install"] === void 0 && noModeFlags && noGitFlags && noVariableFlags && noLogFlags) {
153
+ await (await import("./variables-DcwMeIau.js")).runVariablesList({});
144
154
  return;
145
155
  }
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({
156
+ if (positionals.length === 3 && positionals[0] === "variables" && positionals[1] === "delete" && values["no-install"] === void 0 && noModeFlags && noGitFlags && values.secret === void 0 && noLogFlags) {
157
+ await (await import("./variables-DcwMeIau.js")).runVariablesDelete({
148
158
  key: positionals[2],
149
159
  yes: values.yes
150
160
  });
@@ -1,7 +1,8 @@
1
- import { r as prepareAppletProject } from "./project-mW0vsN8A.js";
1
+ import { i as prepareAppletProjects, r as prepareAppletProject } from "./project-C_rtpjp8.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 };
@@ -0,0 +1,2 @@
1
+ import { i as runBuild } from "./build-DauirTcY.js";
2
+ export { runBuild };
@@ -1,4 +1,4 @@
1
- import { i as resolveInstanceUrl } from "./auth-DaVK0k5R.js";
1
+ import { l as resolveInstanceUrl } from "./workspace-B6lenAS5.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-C489nYUy.js";
2
+ import { t as NarrowContract } from "./contract-DB9QQhD9.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-C489nYUy.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-B6lenAS5.js";
2
+ import { a as loadGitHubActionsSources, n as githubActionsContext, r as hitchResponseError, t as fetchWithGitHubActions } from "./ci-gPrwo6C0.js";
3
+ import { n as buildPreparedApplet, r as prepareWorkspaceBuild, t as buildApplet } from "./build-DauirTcY.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 };
@@ -1,7 +1,7 @@
1
- import { i as resolveInstanceUrl, n as organizationInstanceUrl, t as fetchWithOrgAuth } from "./auth-DaVK0k5R.js";
2
- import { i as loadGitHubActionsBuildInputs, n as githubActionsContext } from "./ci-D0nC_Ho7.js";
3
- import { t as loadAppletConfig } from "./load-config-BnxocLAQ.js";
4
- import { r as prepareAppletProject } from "./project-mW0vsN8A.js";
1
+ import { l as resolveInstanceUrl, n as fetchWithOrgAuth, s as organizationInstanceUrl } from "./workspace-B6lenAS5.js";
2
+ import { i as loadGitHubActionsBuildInputs, n as githubActionsContext } from "./ci-gPrwo6C0.js";
3
+ import { t as loadAppletConfig } from "./load-config-BknBkdRs.js";
4
+ import { r as prepareAppletProject } from "./project-C_rtpjp8.js";
5
5
  import { createRequire } from "node:module";
6
6
  import { existsSync } from "node:fs";
7
7
  import { access, mkdir, mkdtemp, readFile, readdir, rename, rm, writeFile } from "node:fs/promises";
@@ -352,11 +352,11 @@ async function responseJson(response) {
352
352
  throw new Error("Failed to parse the organization's OpenAPI spec.", { cause: error });
353
353
  }
354
354
  }
355
- async function loadAuthenticatedSpec(orgCode, options) {
355
+ async function loadAuthenticatedSpec(manifest, options) {
356
356
  const github = githubActionsContext(options.store?.env ?? process.env);
357
- if (github !== void 0) return (await loadGitHubActionsBuildInputs(github, options)).openapi;
357
+ if (github !== void 0) return (await loadGitHubActionsBuildInputs(github, options, manifest.requiresChangeset)).openapi;
358
358
  const fetcher = options.fetch ?? globalThis.fetch;
359
- const response = await fetchWithOrgAuth(orgCode, options, (instanceUrl, accessToken) => fetcher(`${instanceUrl}/api/v1/openapi.json${options.changesetId === void 0 ? "" : `?changeset_id=${encodeURIComponent(options.changesetId)}`}`, { headers: { Authorization: `Bearer ${accessToken}` } }));
359
+ const response = await fetchWithOrgAuth(manifest.org, options, (instanceUrl, accessToken) => fetcher(`${instanceUrl}/api/v1/openapi.json${manifest.requiresChangeset === void 0 ? "" : `?changeset_id=${encodeURIComponent(manifest.requiresChangeset)}`}`, { headers: { Authorization: `Bearer ${accessToken}` } }));
360
360
  if (!response.ok) throw new Error(`Failed to load the organization's OpenAPI spec (${response.status}).`);
361
361
  return responseJson(response);
362
362
  }
@@ -367,18 +367,18 @@ async function loadThemeStylesheet(orgCode, options) {
367
367
  if (!response.ok) throw new Error(`Failed to load the organization's theme (${response.status}).`);
368
368
  return response.text();
369
369
  }
370
- async function loadBuildInputs(orgCode, options) {
370
+ async function loadBuildInputs(manifest, options) {
371
371
  const github = githubActionsContext(options.store?.env ?? process.env);
372
372
  if (github !== void 0) {
373
- const inputs = await loadGitHubActionsBuildInputs(github, options);
373
+ const inputs = await loadGitHubActionsBuildInputs(github, options, manifest.requiresChangeset);
374
374
  return {
375
375
  spec: inputs.openapi,
376
376
  theme: inputs.theme
377
377
  };
378
378
  }
379
379
  return {
380
- spec: await loadAuthenticatedSpec(orgCode, options),
381
- theme: await loadThemeStylesheet(orgCode, options)
380
+ spec: await loadAuthenticatedSpec(manifest, options),
381
+ theme: await loadThemeStylesheet(manifest.org, options)
382
382
  };
383
383
  }
384
384
  async function generateContract(spec, outDir) {
@@ -442,14 +442,14 @@ export type * from "./zod.gen.ts";
442
442
  `);
443
443
  }
444
444
  async function syncContract(outDir, manifest, options = {}) {
445
- await writeContractFiles(outDir, manifest, await loadAuthenticatedSpec(manifest.org, options));
445
+ await writeContractFiles(outDir, manifest, await loadAuthenticatedSpec(manifest, options));
446
446
  }
447
447
  async function runGenerate(options) {
448
448
  await prepareAppletProject();
449
449
  const root = process.cwd();
450
450
  const outDir = resolve("./.hitch");
451
451
  const manifest = await loadAppletConfig(process.cwd());
452
- const { spec, theme: themeStylesheet } = await loadBuildInputs(manifest.org, options);
452
+ const { spec, theme: themeStylesheet } = await loadBuildInputs(manifest, options);
453
453
  const stagedDir = await mkdtemp(resolve("./.hitch.tmp-"));
454
454
  const backupDir = `${stagedDir}.backup`;
455
455
  let staged = true;
@@ -0,0 +1,2 @@
1
+ import { n as runGenerate } from "./generate-BIifT6PK.js";
2
+ export { runGenerate };
package/dist/index.d.ts CHANGED
@@ -1,9 +1,9 @@
1
- import { _ as RegisteredWorkflows, a as AppletSourceDefinition, c as appletManifestSchema, d as RegisteredCatalog, f as RegisteredContract, g as RegisteredVariables, h as RegisteredVariableKeys, i as AppletPackages, l as appletSourceDefinitionSchema, m as RegisteredRouter, n as AppletManifest, o as RegisteredAppletSourceDefinition, p as RegisteredManifest, r as AppletManifestObject, s as appletDefinitionSchema, t as AppletDefinition, u as HitchRegister, v as registerHitchContract } from "./manifest-BQCAxATS.js";
2
- import { n as pruneContract, t as NarrowContract } from "./contract-DMrpbaLJ.js";
1
+ import { _ as RegisteredWorkflows, a as AppletSourceDefinition, c as appletManifestSchema, d as RegisteredCatalog, f as RegisteredContract, g as RegisteredVariables, h as RegisteredVariableKeys, i as AppletPackages, l as appletSourceDefinitionSchema, m as RegisteredRouter, n as AppletManifest, o as RegisteredAppletSourceDefinition, p as RegisteredManifest, r as AppletManifestObject, s as appletDefinitionSchema, t as AppletDefinition, u as HitchRegister, v as registerHitchContract } from "./manifest-C489nYUy.js";
2
+ import { n as pruneContract, t as NarrowContract } from "./contract-DB9QQhD9.js";
3
3
  import { a as UploadedHitchFile, c as createFileOutputSchema, d as getFileOutputSchema, i as UploadProgress, l as fileSchema, n as FilesUpload, r as UploadFileOptions, s as uploadFile, t as FilesClient, u as filesContract } from "./upload-YbRKEqR6.js";
4
4
  import { ContractProcedure, Meta } from "@orpc/contract";
5
5
  import { z } from "zod";
6
- //#region src/workspace.d.ts
6
+ //#region ../shared/auth/src/workspace.d.mts
7
7
  interface HitchConfig {
8
8
  sdk: string;
9
9
  org: string;
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { i as registerHitchContract, t as uploadFile } from "./upload-DRbVhuDm.js";
2
- import { n as appletManifestSchema, r as appletSourceDefinitionSchema, t as appletDefinitionSchema } from "./manifest-DyoJ1MIL.js";
2
+ import { n as appletManifestSchema, r as appletSourceDefinitionSchema, t as appletDefinitionSchema } from "./manifest-Bd04cWhu.js";
3
3
  import { oc } from "@orpc/contract";
4
4
  import { z } from "zod";
5
5
  //#region src/hitch/contract.ts
@@ -1,8 +1,8 @@
1
- import { a as ORG_CODE_PATTERN, i as APPLET_CODE_PATTERN } from "./manifest-DyoJ1MIL.js";
2
- import { i as resolveInstanceUrl, r as resolveAuthForOrg } from "./auth-DaVK0k5R.js";
3
- import { n as runGenerate } from "./generate-C6eAL5eo.js";
4
- import { n as loadAppletSourceDefinition, r as readWorkspaceConfig } from "./load-config-BnxocLAQ.js";
5
- import { a as writeWorkspaceSupport, i as writeAppletSupport, n as loadCliManifest, t as assertWorkspaceCli } from "./project-mW0vsN8A.js";
1
+ import { a as ORG_CODE_PATTERN, i as APPLET_CODE_PATTERN } from "./manifest-Bd04cWhu.js";
2
+ import { c as resolveAuthForOrg, i as readWorkspaceConfig, l as resolveInstanceUrl } from "./workspace-B6lenAS5.js";
3
+ import { n as runGenerate } from "./generate-BIifT6PK.js";
4
+ import { n as loadAppletSourceDefinition } from "./load-config-BknBkdRs.js";
5
+ import { a as writeAppletSupport, n as loadCliManifest, o as writeWorkspaceSupport, t as assertWorkspaceCli } from "./project-C_rtpjp8.js";
6
6
  import { access, cp, mkdir, readdir, writeFile } from "node:fs/promises";
7
7
  import { relative, resolve } from "node:path";
8
8
  import { spawn } from "node:child_process";
@@ -0,0 +1,27 @@
1
+ import { r as appletSourceDefinitionSchema } from "./manifest-Bd04cWhu.js";
2
+ import { t as resolveWorkspaceOrg } from "./workspace-B6lenAS5.js";
3
+ import { existsSync } from "node:fs";
4
+ import { resolve } from "node:path";
5
+ import { loadConfigFromFile } from "vite";
6
+ //#region src/typegen/load-config.ts
7
+ async function loadAppletSourceDefinition(root) {
8
+ const path = resolve(root, "applet.manifest.ts");
9
+ if (!existsSync(path)) throw new Error(`No applet manifest found at ${path}.`);
10
+ const loaded = await loadConfigFromFile({
11
+ command: "build",
12
+ mode: "production"
13
+ }, path, root);
14
+ if (loaded === null) throw new Error(`No applet manifest found at ${path}.`);
15
+ const parsed = appletSourceDefinitionSchema.safeParse(loaded.config);
16
+ if (!parsed.success) throw new Error(`Invalid applet manifest at ${loaded.path}: ${parsed.error.message}`);
17
+ return parsed.data;
18
+ }
19
+ async function loadAppletConfig(root) {
20
+ const { dependencies: _dependencies, devDependencies: _devDependencies, ...definition } = await loadAppletSourceDefinition(root);
21
+ return {
22
+ org: resolveWorkspaceOrg(root),
23
+ ...definition
24
+ };
25
+ }
26
+ //#endregion
27
+ export { loadAppletSourceDefinition as n, loadAppletConfig as t };
@@ -0,0 +1,12 @@
1
+ import { a as loginWithDevice, l as resolveInstanceUrl, o as AuthenticationError, r as findWorkspaceConfig } from "./workspace-B6lenAS5.js";
2
+ //#region src/login.ts
3
+ async function runLogin(options) {
4
+ if (process.env.HITCH_API_KEY !== void 0) throw new AuthenticationError("HITCH_API_KEY is set — unset it to manage stored logins.");
5
+ const { me, membership } = await loginWithDevice(await resolveInstanceUrl(), {
6
+ expectedOrg: findWorkspaceConfig(process.cwd())?.org,
7
+ noBrowser: options.noBrowser
8
+ });
9
+ console.log(`Signed in as ${me.person.email} to ${membership.name}.`);
10
+ }
11
+ //#endregion
12
+ export { runLogin };
@@ -1,6 +1,6 @@
1
- import { i as APPLET_CODE_PATTERN } from "./manifest-DyoJ1MIL.js";
2
- import { t as fetchWithOrgAuth } from "./auth-DaVK0k5R.js";
3
- import { i as resolveWorkspaceOrg, r as readWorkspaceConfig, t as loadAppletConfig } from "./load-config-BnxocLAQ.js";
1
+ import { i as APPLET_CODE_PATTERN } from "./manifest-Bd04cWhu.js";
2
+ import { i as readWorkspaceConfig, n as fetchWithOrgAuth, t as resolveWorkspaceOrg } from "./workspace-B6lenAS5.js";
3
+ import { t as loadAppletConfig } from "./load-config-BknBkdRs.js";
4
4
  import { z } from "zod";
5
5
  //#region src/logs.ts
6
6
  const LOG_LEVELS = [
@@ -63,6 +63,7 @@ const appletManifestShape = {
63
63
  org: z.string().regex(ORG_CODE_PATTERN, "must start with a lowercase letter, end with a letter or number, and contain only lowercase letters, numbers, and non-consecutive hyphens"),
64
64
  name: nameSchema,
65
65
  code: codeSchema,
66
+ requiresChangeset: z.uuid().optional(),
66
67
  schedules: appletManifestSchedulesSchema.optional(),
67
68
  variables: appletManifestVariablesSchema.optional(),
68
69
  objects: appletManifestObjectsSchema
@@ -94,6 +95,7 @@ const appletManifestSchema = z.strictObject(appletManifestShape).superRefine(che
94
95
  const appletDefinitionSchema = z.strictObject({
95
96
  name: appletManifestShape.name,
96
97
  code: appletManifestShape.code,
98
+ requiresChangeset: appletManifestShape.requiresChangeset,
97
99
  schedules: appletManifestShape.schedules,
98
100
  variables: appletManifestShape.variables,
99
101
  objects: appletManifestShape.objects
@@ -161,6 +163,7 @@ const checkSourcePackages = (manifest, context) => {
161
163
  const appletSourceDefinitionSchema = z.strictObject({
162
164
  name: appletManifestShape.name,
163
165
  code: appletManifestShape.code,
166
+ requiresChangeset: appletManifestShape.requiresChangeset,
164
167
  schedules: appletManifestShape.schedules,
165
168
  variables: appletManifestShape.variables,
166
169
  objects: appletManifestShape.objects,
@@ -32,6 +32,8 @@ interface AppletManifest {
32
32
  org: string;
33
33
  name: string;
34
34
  code: string;
35
+ /** This code needs the schema as of this changeset. */
36
+ requiresChangeset?: string;
35
37
  schedules?: readonly {
36
38
  code: string;
37
39
  name: string;
@@ -1,4 +1,5 @@
1
- import { n as loadAppletSourceDefinition, r as readWorkspaceConfig } from "./load-config-BnxocLAQ.js";
1
+ import { i as readWorkspaceConfig } from "./workspace-B6lenAS5.js";
2
+ import { n as loadAppletSourceDefinition } from "./load-config-BknBkdRs.js";
2
3
  import { z } from "zod";
3
4
  import { existsSync } from "node:fs";
4
5
  import { cp, mkdir, readFile, rm, symlink, writeFile } from "node:fs/promises";
@@ -340,17 +341,17 @@ export default defineConfig({
340
341
  hasClient ? writeFile(resolve(root, "components.json"), await readFile(new URL("../scaffold/applet/components.json", import.meta.url), "utf8")) : void 0
341
342
  ]);
342
343
  }
343
- async function prepareAppletProject(appletRoot = process.cwd()) {
344
- const definition = await loadAppletSourceDefinition(appletRoot);
345
- const workspaceRoot = dirname(dirname(resolve(appletRoot)));
344
+ async function prepareAppletProjects(workspaceRoot, appletRoots) {
345
+ const definitions = await Promise.all(appletRoots.map((root) => loadAppletSourceDefinition(root)));
346
346
  const workspace = readWorkspaceConfig(workspaceRoot);
347
- if (workspace === void 0) throw new Error(`No Hitch workspace found for ${appletRoot}.`);
347
+ if (workspace === void 0) throw new Error(`No Hitch workspace found at ${workspaceRoot}.`);
348
348
  const cli = await loadCliManifest({ allowDevelopment: true });
349
- await Promise.all([cli === void 0 ? void 0 : writeWorkspaceSupport(workspaceRoot, workspace, cli), writeAppletSupport(appletRoot, definition)]);
350
- return {
351
- definition,
352
- workspaceRoot
353
- };
349
+ await Promise.all([cli === void 0 ? void 0 : writeWorkspaceSupport(workspaceRoot, workspace, cli), ...appletRoots.map((root, index) => writeAppletSupport(root, definitions[index]))]);
350
+ }
351
+ async function prepareAppletProject(appletRoot = process.cwd()) {
352
+ const workspaceRoot = dirname(dirname(resolve(appletRoot)));
353
+ await prepareAppletProjects(workspaceRoot, [appletRoot]);
354
+ return workspaceRoot;
354
355
  }
355
356
  //#endregion
356
- export { writeWorkspaceSupport as a, writeAppletSupport as i, loadCliManifest as n, prepareAppletProject as r, assertWorkspaceCli as t };
357
+ export { writeAppletSupport as a, prepareAppletProjects as i, loadCliManifest as n, writeWorkspaceSupport as o, prepareAppletProject as r, assertWorkspaceCli as t };
package/dist/server.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { a as createWorkflow, i as WorkflowStep, n as WorkflowDefinition, r as WorkflowDuration } from "./workflow-DurM6Fwx.js";
2
- import { _ as RegisteredWorkflows, f as RegisteredContract, g as RegisteredVariables, p as RegisteredManifest } from "./manifest-BQCAxATS.js";
3
- import { t as NarrowContract } from "./contract-DMrpbaLJ.js";
2
+ import { _ as RegisteredWorkflows, f as RegisteredContract, g as RegisteredVariables, p as RegisteredManifest } from "./manifest-C489nYUy.js";
3
+ import { t as NarrowContract } from "./contract-DB9QQhD9.js";
4
4
  import { t as WithBatchRecorder } from "./batch-Qa4B4-Yp.js";
5
5
  import { o as WithFilesUpload } from "./upload-YbRKEqR6.js";
6
6
  import { ContractRouterClient } from "@orpc/contract";
@@ -1,8 +1,8 @@
1
- import { t as fetchWithOrgAuth } from "./auth-DaVK0k5R.js";
2
- import { t as loadAppletConfig } from "./load-config-BnxocLAQ.js";
1
+ import { n as fetchWithOrgAuth } from "./workspace-B6lenAS5.js";
2
+ import { t as loadAppletConfig } from "./load-config-BknBkdRs.js";
3
3
  import { z } from "zod";
4
- import { Writable } from "node:stream";
5
4
  import { createInterface } from "node:readline";
5
+ import { Writable } from "node:stream";
6
6
  //#region src/variables.ts
7
7
  const variableRowSchema = z.object({
8
8
  key: z.string(),
package/dist/vite.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { o as RegisteredAppletSourceDefinition } from "./manifest-BQCAxATS.js";
1
+ import { o as RegisteredAppletSourceDefinition } from "./manifest-C489nYUy.js";
2
2
  import { PluginOption } from "vite";
3
3
  //#region ../shared/auth/src/store.d.mts
4
4
  interface SessionCredentials {
package/dist/vite.js CHANGED
@@ -1,7 +1,6 @@
1
- import { i as APPLET_CODE_PATTERN, n as appletManifestSchema, r as appletSourceDefinitionSchema } from "./manifest-DyoJ1MIL.js";
2
- import { i as resolveInstanceUrl, n as organizationInstanceUrl } from "./auth-DaVK0k5R.js";
3
- import { c as writeClientEntry, i as writeRegisterDeclaration, l as writeManifestGen, r as syncContract, s as writeAppletRoot, u as writeServerEntry } from "./generate-C6eAL5eo.js";
4
- import { i as resolveWorkspaceOrg } from "./load-config-BnxocLAQ.js";
1
+ import { i as APPLET_CODE_PATTERN, n as appletManifestSchema, r as appletSourceDefinitionSchema } from "./manifest-Bd04cWhu.js";
2
+ import { l as resolveInstanceUrl, s as organizationInstanceUrl, t as resolveWorkspaceOrg } from "./workspace-B6lenAS5.js";
3
+ import { c as writeClientEntry, i as writeRegisterDeclaration, l as writeManifestGen, r as syncContract, s as writeAppletRoot, u as writeServerEntry } from "./generate-BIifT6PK.js";
5
4
  import { existsSync } from "node:fs";
6
5
  import { appendFile, readFile, readdir, rm, writeFile } from "node:fs/promises";
7
6
  import { basename, relative, resolve } from "node:path";
@@ -221,10 +220,7 @@ function applet(sourceDefinition, options = {}) {
221
220
  }
222
221
  if (environment.command === "build") themeStylesheetUrl = `${organizationInstanceUrl(await resolveInstanceUrl(void 0, options.store), manifest.org)}/theme.css`;
223
222
  if (environment.command === "build" && manifest.objects.length > 0) {
224
- contractSync ??= syncContract(resolve(root, ".hitch"), manifest, {
225
- ...options,
226
- changesetId: (options.store?.env ?? process.env).HITCH_CHANGESET_ID
227
- });
223
+ contractSync ??= syncContract(resolve(root, ".hitch"), manifest, options);
228
224
  await contractSync;
229
225
  }
230
226
  const hasHitchGen = existsSync(resolve(root, ".hitch/index.ts"));
@@ -1,5 +1,7 @@
1
+ import { createRequire } from "node:module";
2
+ import { existsSync } from "node:fs";
1
3
  import { chmod, mkdir, readFile, writeFile } from "node:fs/promises";
2
- import { join } from "node:path";
4
+ import { basename, dirname, join, resolve } from "node:path";
3
5
  import { spawn } from "node:child_process";
4
6
  import { homedir } from "node:os";
5
7
  //#region ../shared/auth/src/store.mts
@@ -326,6 +328,30 @@ async function loginWithDevice(instanceUrl, options = {}) {
326
328
  };
327
329
  }
328
330
  //#endregion
331
+ //#region ../shared/auth/src/workspace.mts
332
+ const ORG_CODE_PATTERN = /^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/;
333
+ const require = createRequire(import.meta.url);
334
+ function readWorkspaceConfig(dir) {
335
+ const path = resolve(dir, "hitch.config.ts");
336
+ if (!existsSync(path)) return void 0;
337
+ let module;
338
+ try {
339
+ module = require(path);
340
+ } catch (error) {
341
+ throw new Error(`Failed to load ${path}.`, { cause: error });
342
+ }
343
+ const config = module !== null && typeof module === "object" && "default" in module ? module.default : module;
344
+ if (config === null || typeof config !== "object" || !("sdk" in config) || typeof config.sdk !== "string" || config.sdk.length === 0 || !("org" in config) || typeof config.org !== "string" || !ORG_CODE_PATTERN.test(config.org)) throw new Error(`Invalid Hitch config in ${path}: "sdk" must be a package spec and "org" an organization code.`);
345
+ return {
346
+ sdk: config.sdk,
347
+ org: config.org
348
+ };
349
+ }
350
+ function findWorkspaceConfig(cwd) {
351
+ const appletsDir = dirname(cwd);
352
+ return readWorkspaceConfig(cwd) ?? (basename(appletsDir) === "applets" ? readWorkspaceConfig(dirname(appletsDir)) : void 0);
353
+ }
354
+ //#endregion
329
355
  //#region src/auth.ts
330
356
  async function fetchWithOrgAuth(orgCode, options, request) {
331
357
  const fetcher = options.fetch ?? globalThis.fetch;
@@ -334,9 +360,10 @@ async function fetchWithOrgAuth(orgCode, options, request) {
334
360
  const auth = await resolveAuthForOrg(instanceUrl, orgCode, options.store);
335
361
  if (auth !== void 0) {
336
362
  const response = await request(organizationUrl, bearerToken(auth));
337
- if (response.status !== 401 && response.status !== 403) return response;
363
+ rejectEnvironmentKey(response, auth);
364
+ if (response.status !== 401) return response;
338
365
  }
339
- if (!(options.stdinIsTTY ?? process.stdin.isTTY === true)) throw new Error("not logged in — set HITCH_API_KEY or run in a terminal to log in");
366
+ if (!(options.stdinIsTTY ?? process.stdin.isTTY === true)) throw new AuthenticationError("Not logged in. Run applet login, or set HITCH_API_KEY.");
340
367
  await (options.login ?? loginWithDevice)(instanceUrl, {
341
368
  expectedOrg: orgCode,
342
369
  fetch: fetcher,
@@ -347,4 +374,14 @@ async function fetchWithOrgAuth(orgCode, options, request) {
347
374
  return request(organizationUrl, bearerToken(credentials));
348
375
  }
349
376
  //#endregion
350
- export { resolveInstanceUrl as i, organizationInstanceUrl as n, resolveAuthForOrg as r, fetchWithOrgAuth as t };
377
+ //#region src/workspace.ts
378
+ function resolveWorkspaceOrg(appletRoot) {
379
+ const appletsDir = dirname(appletRoot);
380
+ if (basename(appletsDir) === "applets") {
381
+ const config = readWorkspaceConfig(dirname(appletsDir));
382
+ if (config !== void 0) return config.org;
383
+ }
384
+ throw new Error(`No Hitch workspace found for ${appletRoot}. An applet must live at <workspace>/applets/<code> with hitch.config.ts in the workspace root.`);
385
+ }
386
+ //#endregion
387
+ export { loginWithDevice as a, resolveAuthForOrg as c, readWorkspaceConfig as i, resolveInstanceUrl as l, fetchWithOrgAuth as n, AuthenticationError as o, findWorkspaceConfig as r, organizationInstanceUrl as s, resolveWorkspaceOrg as t };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hitch42/applet",
3
- "version": "0.1.1-beta.6",
3
+ "version": "0.1.1-beta.7",
4
4
  "license": "MIT",
5
5
  "bin": {
6
6
  "applet": "./dist/applet.js"
@@ -30,6 +30,8 @@ For attribute types, filters, and API limits, read `../hitch-data/SKILL.md`. To
30
30
  ## Work with the workspace
31
31
 
32
32
  - Run package commands with `pnpm`.
33
+ - Run `pnpm exec applet login` from the workspace root or an applet directory to sign in. It uses the organization in `hitch.config.ts` and shares credentials with `hitch login`. Add `--no-browser` to print the sign-in URL without opening a browser.
34
+ - A server permission error is not a login request. After an applet has a CI release, the server rejects local deployments; deploy subsequent releases through CI.
33
35
  - If `package.json` is absent, read `sdk` from `hitch.config.ts`. Run `pnpm dlx <sdk> bootstrap` from the workspace root.
34
36
  - Run `pnpm exec applet new <code>` from the workspace root to create a full applet.
35
37
  - Add `--client-only` or `--server-only` when the applet needs only one part.
@@ -138,3 +140,11 @@ pnpm --dir applets/<code> run build
138
140
  If missing packages block a check, stop package checks. Do not retry the check through another binary.
139
141
 
140
142
  If a command fails, report the exact blocker. Do not deploy as a verification step.
143
+
144
+ ## Deploy from GitHub
145
+
146
+ - A push to the production branch runs `applet deploy` in the workspace root through GitHub Actions.
147
+ - An applet deploys when its own folder changed since its current CI release. Every applet deploys when a file outside `applets/` changed. An applet without a CI release always deploys.
148
+ - The deploy skips other applets and logs the reason. A skip is expected, not a failure.
149
+ - A schema change in Hitch alone does not trigger CI.
150
+ - Do not import from a sibling applet folder. The change check does not follow such imports.
@@ -11,6 +11,7 @@ The applet tool reads `org` from `hitch.config.ts` in the workspace root.
11
11
  - `name` gives the applet a display name.
12
12
  - `code` gives the applet its stable code and URL prefix.
13
13
  - `objects` declares record objects and attribute access.
14
+ - `requiresChangeset` optionally names the schema changeset that the code needs.
14
15
  - `variables` optionally declares runtime variables.
15
16
  - `schedules` optionally declares scheduled server calls.
16
17
  - `dependencies` optionally declares applet runtime packages.
@@ -42,6 +43,22 @@ The manifest and applet definition reject unknown fields.
42
43
  - Expect the Vite base path to be `/<code>/`.
43
44
  - Include the code in each client route path.
44
45
 
46
+ ## Set `requiresChangeset`
47
+
48
+ Set `requiresChangeset` when the code needs a schema change that is not live yet.
49
+
50
+ ```ts
51
+ requiresChangeset: "0192a1b2-c3d4-7e5f-8a9b-0c1d2e3f4a5b",
52
+ ```
53
+
54
+ - Use the ID of a changeset in the same organization.
55
+ - Generate, build, and deploy all use this field. The CLI has no changeset flag.
56
+ - While the changeset is pending, types and the build use the schema that the changeset will produce.
57
+ - While the changeset is pending, a deploy binds the release to the changeset. The release goes live when a person applies the changeset in Hitch.
58
+ - After the changeset applies, the field uses the live schema. A deploy then promotes as usual. Remove the field when it is convenient.
59
+ - A deploy fails when the changeset was abandoned. Remove the field or name a pending changeset.
60
+ - A git push uses the same rules. The push never applies the changeset.
61
+
45
62
  ## Declare objects
46
63
 
47
64
  Add one entry for each object that the applet needs.
@@ -12,8 +12,9 @@ The `hitch` command talks to one organization on one Hitch instance. Its data an
12
12
  ## Sign in
13
13
 
14
14
  - Run `hitch login` to sign in with the device flow. The command prints a URL and a code. Open the URL and confirm the code. Add `--no-browser` when the shell cannot open a browser.
15
+ - `applet login` uses this same device flow and credential store. Both commands preselect the organization from `hitch.config.ts` at the workspace root or in an applet directory. Use `hitch login --org <code>` to select it explicitly.
15
16
  - Run `hitch login --api-key` to store an API key instead. The command prompts for the key. Pass `--api-key <key>` to skip the prompt.
16
- - Set `HITCH_API_KEY` to use a key for one shell session. It overrides every stored login. While it is set, `hitch login` and `hitch logout` refuse to run.
17
+ - Set `HITCH_API_KEY` to use a key for one shell session. It overrides every stored login. While it is set, `hitch login`, `applet login`, and `hitch logout` refuse to run.
17
18
  - Run `hitch whoami` to print the current person, email, organization name, and organization id as JSON.
18
19
  - Run `hitch logout` to remove every stored login and cached command set.
19
20
  - Set `HITCH_API_URL` or pass `--instance <url>` to `hitch login` for an instance other than the default.
@@ -23,7 +24,7 @@ The `hitch` command talks to one organization on one Hitch instance. Its data an
23
24
  The CLI resolves the organization in this order. The first match wins.
24
25
 
25
26
  1. `--org <code>` on any command.
26
- 2. The workspace organization: the `hitch.org` field in the nearest `package.json`, or in the workspace root when the current directory is an applet under `applets/`.
27
+ 2. The workspace organization: the `org` field in `hitch.config.ts` in the current directory, or in the workspace root when the current directory is an applet under `applets/`.
27
28
  3. `HITCH_DEFAULT_ORG`.
28
29
 
29
30
  There is no remembered active organization. When `--org` and the workspace disagree, the command fails with `--org "x" does not match workspace organization "y"`. When several organizations are stored and none is selected, the command fails and names the three ways to choose.
@@ -110,6 +111,7 @@ hitch admin changesets update --id <changesetId> --adopt <otherChangesetId>
110
111
  - Run `blockers list` before an apply. Any blocker stops the apply with HTTP 409.
111
112
  - Run `hitch sync` after an apply, so the new objects and attributes appear as commands.
112
113
  - Run `pnpm --dir applets/<code> run generate` after an apply, so applet types match.
114
+ - To build applet code before the apply, set `requiresChangeset` in `applet.manifest.ts` to the changeset ID. Generate, build, and deploy then use the changeset schema, and the release goes live when the changeset applies.
113
115
 
114
116
  ## Fix common errors
115
117
 
@@ -119,7 +121,7 @@ hitch admin changesets update --id <changesetId> --adopt <otherChangesetId>
119
121
  | `HITCH_API_KEY was rejected.` | The key in the environment is invalid or revoked. Unset it or replace it. |
120
122
  | `API key is scoped to organization "x", not "y".` | The key belongs to another organization. Select `x` or use another key. |
121
123
  | `--org "x" does not match workspace organization` | Run the command outside the workspace, or drop `--org`. |
122
- | `Multiple organizations are stored ...` | Pass `--org`, set `hitch.org`, or set `HITCH_DEFAULT_ORG`. |
124
+ | `Multiple organizations are stored ...` | Pass `--org`, use the workspace `hitch.config.ts`, or set `HITCH_DEFAULT_ORG`. |
123
125
  | `the cached commands ... no longer load` | Run `hitch sync`. |
124
126
  | `Refusing to delete without --yes` | Add `--yes` in a script. |
125
127
  | `hitch <object>` is an unknown command | Run `hitch sync`. The object is new or the cache is stale. |
@@ -48,4 +48,4 @@ An organization's data lives in Hitch objects. Each object has attributes. Each
48
48
 
49
49
  - Every object or attribute change goes through a changeset. Hitch evaluates the change against existing records and dependent applets.
50
50
  - A change that would break existing data, such as a unique constraint with duplicates, is blocked until the data is fixed.
51
- - An applet release can bind to a pending changeset. The changeset apply switches the schema and promotes the release together.
51
+ - An applet release can bind to a pending changeset. Set `requiresChangeset` in `applet.manifest.ts` to bind it. The changeset apply switches the schema and promotes the release together.
@@ -2,12 +2,6 @@ name: Deploy Hitch applets
2
2
 
3
3
  on:
4
4
  push:
5
- paths:
6
- - "applets/**"
7
- - ".github/workflows/hitch.yml"
8
- - "hitch.config.ts"
9
- - "pnpm-lock.yaml"
10
- - "pnpm-workspace.yaml"
11
5
 
12
6
  permissions: {}
13
7
 
@@ -1,2 +0,0 @@
1
- import { n as runBuild } from "./build-DIsKQQT0.js";
2
- export { runBuild };
@@ -1,2 +0,0 @@
1
- import { n as runGenerate } from "./generate-C6eAL5eo.js";
2
- export { runGenerate };
@@ -1,56 +0,0 @@
1
- import { a as ORG_CODE_PATTERN, r as appletSourceDefinitionSchema } from "./manifest-DyoJ1MIL.js";
2
- import { createRequire } from "node:module";
3
- import { z } from "zod";
4
- import { existsSync } from "node:fs";
5
- import { basename, dirname, resolve } from "node:path";
6
- import { loadConfigFromFile } from "vite";
7
- //#region src/workspace.ts
8
- const hitchConfigSchema = z.object({
9
- sdk: z.string().min(1),
10
- org: z.string().regex(ORG_CODE_PATTERN)
11
- });
12
- const require = createRequire(import.meta.url);
13
- function readWorkspaceConfig(dir) {
14
- const path = resolve(dir, "hitch.config.ts");
15
- if (!existsSync(path)) return void 0;
16
- let module;
17
- try {
18
- module = require(path);
19
- } catch (error) {
20
- throw new Error(`Failed to load ${path}.`, { cause: error });
21
- }
22
- const config = hitchConfigSchema.safeParse(module !== null && typeof module === "object" && "default" in module ? module.default : module);
23
- if (!config.success) throw new Error(`Invalid Hitch config in ${path}: ${config.error.message}`);
24
- return config.data;
25
- }
26
- function resolveWorkspaceOrg(appletRoot) {
27
- const appletsDir = dirname(appletRoot);
28
- if (basename(appletsDir) === "applets") {
29
- const config = readWorkspaceConfig(dirname(appletsDir));
30
- if (config !== void 0) return config.org;
31
- }
32
- throw new Error(`No Hitch workspace found for ${appletRoot}. An applet must live at <workspace>/applets/<code> with hitch.config.ts in the workspace root.`);
33
- }
34
- //#endregion
35
- //#region src/typegen/load-config.ts
36
- async function loadAppletSourceDefinition(root) {
37
- const path = resolve(root, "applet.manifest.ts");
38
- if (!existsSync(path)) throw new Error(`No applet manifest found at ${path}.`);
39
- const loaded = await loadConfigFromFile({
40
- command: "build",
41
- mode: "production"
42
- }, path, root);
43
- if (loaded === null) throw new Error(`No applet manifest found at ${path}.`);
44
- const parsed = appletSourceDefinitionSchema.safeParse(loaded.config);
45
- if (!parsed.success) throw new Error(`Invalid applet manifest at ${loaded.path}: ${parsed.error.message}`);
46
- return parsed.data;
47
- }
48
- async function loadAppletConfig(root) {
49
- const { dependencies: _dependencies, devDependencies: _devDependencies, ...definition } = await loadAppletSourceDefinition(root);
50
- return {
51
- org: resolveWorkspaceOrg(root),
52
- ...definition
53
- };
54
- }
55
- //#endregion
56
- export { resolveWorkspaceOrg as i, loadAppletSourceDefinition as n, readWorkspaceConfig as r, loadAppletConfig as t };