@hitch42/cli 0.1.1-beta.1 → 0.1.1-beta.11

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 (2) hide show
  1. package/dist/hitch.js +114 -136
  2. package/package.json +3 -3
package/dist/hitch.js CHANGED
@@ -1,10 +1,11 @@
1
1
  #!/usr/bin/env node
2
- import { registerHooks } from "node:module";
2
+ import { createRequire, registerHooks } from "node:module";
3
3
  import { access, chmod, mkdir, readFile, readdir, realpath, rm, stat, writeFile } from "node:fs/promises";
4
4
  import { basename, dirname, join, resolve } from "node:path";
5
5
  import { pathToFileURL } from "node:url";
6
6
  import { spawn } from "node:child_process";
7
7
  import { homedir } from "node:os";
8
+ import { existsSync } from "node:fs";
8
9
  import { os } from "@orpc/server";
9
10
  import { createCli } from "trpc-cli";
10
11
  import { createInterface } from "node:readline/promises";
@@ -13,7 +14,6 @@ import { isContractProcedure } from "@orpc/contract";
13
14
  import { OpenAPILink } from "@orpc/openapi-client/fetch";
14
15
  import { z } from "zod";
15
16
  import { createClient } from "@hey-api/openapi-ts";
16
- import { readFileSync } from "node:fs";
17
17
  //#region ../shared/auth/src/store.mts
18
18
  const DEFAULT_INSTANCE_URL = "https://app.hitch42.com";
19
19
  var AuthenticationError = class extends Error {};
@@ -55,8 +55,8 @@ async function healCredentialStore(credentialsPath, source, store, unrecognizedC
55
55
  console.error(unrecognizedCount === void 0 ? `hitch: reset unreadable credentials file (backup: ${backupPath}).` : unrecognizedCount === 0 ? `hitch: migrated credentials to per-organization storage (backup: ${backupPath}).` : `hitch: removed ${unrecognizedCount} unrecognized credential ${unrecognizedCount === 1 ? "entry" : "entries"} (backup: ${backupPath}).`);
56
56
  return store;
57
57
  }
58
- async function readRawCredentialStore(options = {}) {
59
- const { credentialsPath } = storePaths(options);
58
+ async function readRawCredentialStore() {
59
+ const { credentialsPath } = storePaths();
60
60
  let source;
61
61
  try {
62
62
  source = await readFile(credentialsPath, "utf8");
@@ -123,9 +123,9 @@ function organizationInstanceUrl(instanceUrl, orgCode) {
123
123
  url.hostname = [orgCode, ...labels.slice(1)].join(".");
124
124
  return url.toString().replace(/\/$/, "");
125
125
  }
126
- function storePaths(options = {}) {
127
- const env = options.env ?? process.env;
128
- const home = options.homeDir ?? homedir();
126
+ function storePaths() {
127
+ const env = process.env;
128
+ const home = homedir();
129
129
  const configDir = join(env.XDG_CONFIG_HOME || join(home, ".config"), "hitch");
130
130
  return {
131
131
  configDir,
@@ -134,25 +134,25 @@ function storePaths(options = {}) {
134
134
  credentialsPath: join(configDir, "credentials.json")
135
135
  };
136
136
  }
137
- async function resolveInstanceUrl(override, options = {}) {
137
+ async function resolveInstanceUrl(override) {
138
138
  if (override !== void 0) return normalizeInstanceUrl(override);
139
- const envInstanceUrl = (options.env ?? process.env).HITCH_API_URL;
139
+ const envInstanceUrl = process.env.HITCH_API_URL;
140
140
  if (envInstanceUrl !== void 0 && envInstanceUrl.length > 0) return normalizeInstanceUrl(envInstanceUrl);
141
- const { configPath } = storePaths(options);
141
+ const { configPath } = storePaths();
142
142
  const config = await readJson(configPath);
143
143
  if (config === void 0) return DEFAULT_INSTANCE_URL;
144
144
  if (!isObject$2(config) || config.instanceUrl !== void 0 && typeof config.instanceUrl !== "string") throw new Error(`${configPath}: expected { instanceUrl?: string }.`);
145
145
  return normalizeInstanceUrl(config.instanceUrl ?? "https://app.hitch42.com");
146
146
  }
147
- async function readCredentialStore(options = {}) {
148
- const store = await readRawCredentialStore(options);
147
+ async function readCredentialStore() {
148
+ const store = await readRawCredentialStore();
149
149
  return { instances: Object.fromEntries(Object.entries(store.instances).map(([instanceUrl, credentialsByOrg]) => [instanceUrl, Object.fromEntries(Object.entries(credentialsByOrg).flatMap(([orgCode, value]) => {
150
150
  const credentials = parseCredentials(value);
151
151
  return credentials === void 0 ? [] : [[orgCode, credentials]];
152
152
  }))])) };
153
153
  }
154
- function environmentAuth(options) {
155
- const apiKey = (options.env ?? process.env).HITCH_API_KEY;
154
+ function environmentAuth() {
155
+ const apiKey = process.env.HITCH_API_KEY;
156
156
  return apiKey !== void 0 && apiKey.length > 0 ? {
157
157
  accessToken: apiKey,
158
158
  source: "environment"
@@ -176,18 +176,18 @@ function resolveStoredAuthForOrg(credentialsByOrg, orgCode) {
176
176
  const apiKeyOrgs = Object.values(credentialsByOrg).filter((credentials) => credentials.type === "apiKey").map(({ orgCode: storedOrg }) => storedOrg).sort();
177
177
  if (apiKeyOrgs.length > 0) throw new AuthenticationError(`Stored API ${apiKeyOrgs.length === 1 ? "key is" : "keys are"} scoped to ${apiKeyOrgs.map((storedOrg) => `"${storedOrg}"`).join(", ")}, not "${orgCode}".`);
178
178
  }
179
- async function resolveAuth(instanceUrl, options = {}, orgCode) {
180
- const auth = environmentAuth(options);
179
+ async function resolveAuth(instanceUrl, orgCode) {
180
+ const auth = environmentAuth();
181
181
  if (auth !== void 0) return auth;
182
182
  const normalizedUrl = normalizeInstanceUrl(instanceUrl);
183
- const credentialsByOrg = (await readCredentialStore(options)).instances[normalizedUrl] ?? {};
184
- const selectedOrg = orgCode ?? (options.env ?? process.env).HITCH_DEFAULT_ORG;
183
+ const credentialsByOrg = (await readCredentialStore()).instances[normalizedUrl] ?? {};
184
+ const selectedOrg = orgCode ?? process.env.HITCH_DEFAULT_ORG;
185
185
  if (selectedOrg !== void 0 && selectedOrg.length > 0) return resolveStoredAuthForOrg(credentialsByOrg, selectedOrg);
186
186
  const orgCodes = Object.keys(credentialsByOrg).sort();
187
187
  const soleOrg = orgCodes.at(0);
188
188
  if (soleOrg === void 0) return void 0;
189
189
  if (orgCodes.length === 1) return resolvedStoredAuth(credentialsByOrg[soleOrg]);
190
- throw new AuthenticationError(`Multiple organizations are stored for ${normalizedUrl}: ${orgCodes.join(", ")}. Use --org, a workspace hitch.org, or HITCH_DEFAULT_ORG to choose one.`);
190
+ throw new AuthenticationError(`Multiple organizations are stored for ${normalizedUrl}: ${orgCodes.join(", ")}. Use --org, the workspace hitch.config.ts, or HITCH_DEFAULT_ORG to choose one.`);
191
191
  }
192
192
  function rejectEnvironmentKey(response, auth) {
193
193
  if (auth.source === "environment" && response.status === 401) throw new AuthenticationError("HITCH_API_KEY was rejected.");
@@ -195,9 +195,9 @@ function rejectEnvironmentKey(response, auth) {
195
195
  function bearerToken(credentials) {
196
196
  return "apiKey" in credentials ? credentials.apiKey : credentials.accessToken;
197
197
  }
198
- async function setCredentials(instanceUrl, credentials, options = {}) {
199
- const paths = storePaths(options);
200
- const store = await readRawCredentialStore(options);
198
+ async function setCredentials(instanceUrl, credentials) {
199
+ const paths = storePaths();
200
+ const store = await readRawCredentialStore();
201
201
  const normalizedUrl = normalizeInstanceUrl(instanceUrl);
202
202
  const credentialsByOrg = store.instances[normalizedUrl];
203
203
  store.instances[normalizedUrl] = {
@@ -208,9 +208,9 @@ async function setCredentials(instanceUrl, credentials, options = {}) {
208
208
  await writeFile(paths.credentialsPath, `${JSON.stringify(store, null, 2)}\n`, { mode: 384 });
209
209
  await chmod(paths.credentialsPath, 384);
210
210
  }
211
- async function deleteCredentials(instanceUrl, orgCode, options = {}) {
212
- const paths = storePaths(options);
213
- const store = await readRawCredentialStore(options);
211
+ async function deleteCredentials(instanceUrl, orgCode) {
212
+ const paths = storePaths();
213
+ const store = await readRawCredentialStore();
214
214
  const normalizedUrl = normalizeInstanceUrl(instanceUrl);
215
215
  if (orgCode === void 0) delete store.instances[normalizedUrl];
216
216
  else {
@@ -232,17 +232,17 @@ function instanceSlug(instanceUrl) {
232
232
  const url = new URL(normalizeInstanceUrl(instanceUrl));
233
233
  return `${url.protocol.slice(0, -1)}-${url.host}${url.pathname}`.replace(/[^a-zA-Z0-9.-]+/g, "-").replace(/^-+|-+$/g, "");
234
234
  }
235
- function instanceCacheDir(instanceUrl, options = {}) {
236
- return join(storePaths(options).cacheDir, instanceSlug(instanceUrl));
235
+ function instanceCacheDir(instanceUrl) {
236
+ return join(storePaths().cacheDir, instanceSlug(instanceUrl));
237
237
  }
238
- function contractCacheDir(instanceUrl, organizationId, options = {}) {
239
- return join(instanceCacheDir(instanceUrl, options), organizationId);
238
+ function contractCacheDir(instanceUrl, organizationId) {
239
+ return join(instanceCacheDir(instanceUrl), organizationId);
240
240
  }
241
- function adminContractCacheDir(instanceUrl, organizationId, options = {}) {
242
- return join(contractCacheDir(instanceUrl, organizationId, options), "admin");
241
+ function adminContractCacheDir(instanceUrl, organizationId) {
242
+ return join(contractCacheDir(instanceUrl, organizationId), "admin");
243
243
  }
244
- async function deleteInstanceCache(instanceUrl, options = {}) {
245
- await rm(instanceCacheDir(instanceUrl, options), {
244
+ async function deleteInstanceCache(instanceUrl) {
245
+ await rm(instanceCacheDir(instanceUrl), {
246
246
  recursive: true,
247
247
  force: true
248
248
  });
@@ -365,17 +365,17 @@ async function authWithOrganization(fetcher, instanceUrl, auth, expectedOrg) {
365
365
  };
366
366
  }
367
367
  async function loginWithDevice(instanceUrl, options = {}) {
368
- const fetcher = options.fetch ?? globalThis.fetch;
369
- const now = options.now ?? Date.now;
368
+ const fetcher = globalThis.fetch;
369
+ const now = Date.now;
370
370
  const { body, response } = await postJson(fetcher, `${instanceUrl}/api/auth/device/code`, { client_id: CLIENT_ID });
371
371
  if (!response.ok) throw new AuthenticationError(typeof body.error_description === "string" ? body.error_description : `Could not start sign-in (${response.status}).`);
372
372
  const device = parseDeviceCode(body);
373
373
  const verificationUrl = options.expectedOrg === void 0 ? device.verification_uri_complete : `${device.verification_uri_complete}&org=${encodeURIComponent(options.expectedOrg)}`;
374
- (options.print ?? console.log)(`Open ${verificationUrl} and confirm code ${device.user_code}.`);
374
+ console.log(`Open ${verificationUrl} and confirm code ${device.user_code}.`);
375
375
  if (!options.noBrowser) try {
376
- await (options.openUrl ?? defaultOpenUrl)(verificationUrl);
376
+ defaultOpenUrl(verificationUrl);
377
377
  } catch {}
378
- const token = await pollForToken(fetcher, instanceUrl, device, now, options.sleep ?? ((milliseconds) => new Promise((resolve) => setTimeout(resolve, milliseconds))));
378
+ const token = await pollForToken(fetcher, instanceUrl, device, now, (milliseconds) => new Promise((resolve) => setTimeout(resolve, milliseconds)));
379
379
  const { body: organizationBody, response: organizationResponse } = await postJson(fetcher, `${instanceUrl}/api/auth/device/organization`, { device_code: device.device_code });
380
380
  if (!organizationResponse.ok) throw new AuthenticationError("The sign-in did not resolve an organization.");
381
381
  const organization = parseDeviceOrganization(organizationBody);
@@ -393,7 +393,7 @@ async function loginWithDevice(instanceUrl, options = {}) {
393
393
  organizationId: organization.organizationId,
394
394
  orgCode: organization.orgCode
395
395
  };
396
- await setCredentials(instanceUrl, credentials, options.store);
396
+ await setCredentials(instanceUrl, credentials);
397
397
  if (options.expectedOrg !== void 0 && organization.orgCode !== options.expectedOrg) throw new AuthenticationError(`logged in to "${organization.orgCode}" but this applet targets "${options.expectedOrg}" — confirm the right organization on the device page`);
398
398
  return {
399
399
  credentials,
@@ -402,6 +402,33 @@ async function loginWithDevice(instanceUrl, options = {}) {
402
402
  };
403
403
  }
404
404
  //#endregion
405
+ //#region ../shared/auth/src/workspace.mts
406
+ const ORG_CODE_PATTERN = /^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/;
407
+ const require = createRequire(import.meta.url);
408
+ function readWorkspaceConfig(dir) {
409
+ const path = resolve(dir, "hitch.config.ts");
410
+ if (!existsSync(path)) return void 0;
411
+ let module;
412
+ try {
413
+ module = require(path);
414
+ } catch (error) {
415
+ throw new Error(`Failed to load ${path}.`, { cause: error });
416
+ }
417
+ const config = module !== null && typeof module === "object" && "default" in module ? module.default : module;
418
+ 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.`);
419
+ const githubWorkflow = "githubWorkflow" in config ? config.githubWorkflow : void 0;
420
+ if (githubWorkflow !== void 0 && typeof githubWorkflow !== "boolean") throw new Error(`Invalid Hitch config in ${path}: "githubWorkflow" must be a boolean.`);
421
+ return {
422
+ sdk: config.sdk,
423
+ org: config.org,
424
+ githubWorkflow
425
+ };
426
+ }
427
+ function findWorkspaceConfig(cwd) {
428
+ const appletsDir = dirname(cwd);
429
+ return readWorkspaceConfig(cwd) ?? (basename(appletsDir) === "applets" ? readWorkspaceConfig(dirname(appletsDir)) : void 0);
430
+ }
431
+ //#endregion
405
432
  //#region src/bridge.ts
406
433
  async function defaultPrompt$1(message) {
407
434
  const readline = createInterface({
@@ -493,7 +520,7 @@ function createBridgeRouter(contract, options) {
493
520
  url: `${options.instanceUrl}/api`,
494
521
  headers: { Authorization: `Bearer ${bearerToken(options.auth)}` },
495
522
  fetch: async (request, init) => {
496
- const response = await (options.fetch ?? globalThis.fetch)(request, init);
523
+ const response = await globalThis.fetch(request, init);
497
524
  if ("source" in options.auth) rejectEnvironmentKey(response, options.auth);
498
525
  return response;
499
526
  }
@@ -508,9 +535,9 @@ function createBridgeRouter(contract, options) {
508
535
  const description = node["~orpc"].route.summary ?? node["~orpc"].route.description;
509
536
  const procedure = os.meta(description ? { description } : {}).input(cliInputSchema(inputSchema)).handler(async ({ input }) => {
510
537
  if (isDelete && !input.yes) {
511
- if (options.prompt === void 0 && process.stdin.isTTY !== true) throw new Error("Refusing to delete without --yes in non-interactive mode.");
538
+ if (process.stdin.isTTY !== true) throw new Error("Refusing to delete without --yes in non-interactive mode.");
512
539
  const values = Object.entries(input).filter(([key, value]) => key !== "yes" && value !== void 0).map(([key, value]) => `${key}=${String(value)}`).join(" ");
513
- const answer = await (options.prompt ?? defaultPrompt$1)(`Confirm ${path.join(" ")}${values.length === 0 ? "" : ` ${values}`}? [y/N] `);
540
+ const answer = await defaultPrompt$1(`Confirm ${path.join(" ")}${values.length === 0 ? "" : ` ${values}`}? [y/N] `);
514
541
  if (!["y", "yes"].includes(answer.trim().toLowerCase())) throw new Error("Aborted.");
515
542
  }
516
543
  const detailed = Object.fromEntries(sections.flatMap((section) => {
@@ -529,7 +556,7 @@ function createBridgeRouter(contract, options) {
529
556
  }
530
557
  //#endregion
531
558
  //#region src/generate.ts
532
- const RECORD_ROUTE = /^\/v1\/([^/]+)(?:\/(\{id\}|query|update|aggregate))?$/;
559
+ const RECORD_ROUTE = /^\/v1\/([^/]+)(?:\/(\{id\}(?:\/history)?|query|update|aggregate))?$/;
533
560
  const BATCH_PATH = "/v1/batch";
534
561
  const BATCH_ITEM_PATH = "paths./v1/batch.post.requestBody.content.application/json.schema.properties.requests.items";
535
562
  const BATCH_ID_PATH = /^\^\/v1\/([^/]+)\/\[0-9a-fA-F-\]\{36\}\$$/;
@@ -537,6 +564,7 @@ const VERBS = {
537
564
  "get:": "list",
538
565
  "post:": "create",
539
566
  "get:{id}": "get",
567
+ "get:{id}/history": "history",
540
568
  "patch:{id}": "update",
541
569
  "delete:{id}": "delete",
542
570
  "post:query": "query",
@@ -646,7 +674,7 @@ function prepareRecordSpec(spec) {
646
674
  const prepared = flattenTrivialAllOf(cloned, isObject$1(cloned.components) && isObject$1(cloned.components.schemas) ? cloned.components.schemas : {});
647
675
  prepared.paths = Object.fromEntries(Object.entries(prepared.paths).flatMap(([path, pathItem]) => {
648
676
  if (path === BATCH_PATH) {
649
- if (!isObject$1(pathItem) || pathItem.post === void 0) return [];
677
+ if (!isObject$1(pathItem)) return [];
650
678
  const operation = prepareBatchOperation(pathItem.post);
651
679
  return operation === void 0 ? [] : [[path, { post: operation }]];
652
680
  }
@@ -774,14 +802,14 @@ function objectCount(spec) {
774
802
  return operation ? [operation.objectCode] : [];
775
803
  }) : [])).size;
776
804
  }
777
- async function syncContractCache(instanceUrl, credentials, options = {}) {
805
+ async function syncContractCache(instanceUrl, credentials) {
778
806
  if (credentials.organizationId === void 0) throw new Error("Stored credentials do not include an organization. Run hitch login again.");
779
- const response = await (options.fetch ?? globalThis.fetch)(`${instanceUrl}/api/v1/openapi.json`, { headers: { Authorization: `Bearer ${bearerToken(credentials)}` } });
807
+ const response = await globalThis.fetch(`${instanceUrl}/api/v1/openapi.json`, { headers: { Authorization: `Bearer ${bearerToken(credentials)}` } });
780
808
  if ("source" in credentials) rejectEnvironmentKey(response, credentials);
781
809
  if (!response.ok) throw new Error(`Failed to fetch the organization contract (${response.status} ${response.statusText}).`);
782
810
  const prepared = prepareRecordSpec(await response.json());
783
811
  const count = objectCount(prepared);
784
- const outDir = contractCacheDir(instanceUrl, credentials.organizationId, options.store);
812
+ const outDir = contractCacheDir(instanceUrl, credentials.organizationId);
785
813
  await mkdir(outDir, { recursive: true });
786
814
  await Promise.all((await readdir(outDir)).flatMap((name) => name === "admin" ? [] : [rm(resolve(outDir, name), {
787
815
  recursive: true,
@@ -794,16 +822,16 @@ async function syncContractCache(instanceUrl, credentials, options = {}) {
794
822
  }, null, 2)}\n`);
795
823
  return count;
796
824
  }
797
- async function syncAdminContractCache(instanceUrl, credentials, options = {}) {
825
+ async function syncAdminContractCache(instanceUrl, credentials) {
798
826
  if (credentials.organizationId === void 0) throw new Error("Stored credentials do not include an organization. Run hitch login again.");
799
- const outDir = adminContractCacheDir(instanceUrl, credentials.organizationId, options.store);
827
+ const outDir = adminContractCacheDir(instanceUrl, credentials.organizationId);
800
828
  let response;
801
829
  try {
802
- response = await (options.fetch ?? globalThis.fetch)(`${instanceUrl}/api/admin/v1/openapi.json`, { headers: { Authorization: `Bearer ${bearerToken(credentials)}` } });
830
+ response = await globalThis.fetch(`${instanceUrl}/api/admin/v1/openapi.json`, { headers: { Authorization: `Bearer ${bearerToken(credentials)}` } });
803
831
  if ("source" in credentials) rejectEnvironmentKey(response, credentials);
804
832
  } catch (error) {
805
833
  if (error instanceof AuthenticationError) throw error;
806
- (options.warn ?? console.error)(`Warning: failed to probe admin commands: ${error instanceof Error ? error.message : String(error)}`);
834
+ console.error(`Warning: failed to probe admin commands: ${error instanceof Error ? error.message : String(error)}`);
807
835
  return false;
808
836
  }
809
837
  if (response.status === 401 || response.status === 403) {
@@ -814,7 +842,7 @@ async function syncAdminContractCache(instanceUrl, credentials, options = {}) {
814
842
  return false;
815
843
  }
816
844
  if (!response.ok) {
817
- (options.warn ?? console.error)(`Warning: failed to probe admin commands (${response.status} ${response.statusText}).`);
845
+ console.error(`Warning: failed to probe admin commands (${response.status} ${response.statusText}).`);
818
846
  return false;
819
847
  }
820
848
  const prepared = prepareAdminSpec(await response.json());
@@ -840,32 +868,31 @@ async function defaultPrompt(message) {
840
868
  readline.close();
841
869
  }
842
870
  }
843
- function rejectEnvironmentCredentialManagement(options = {}) {
844
- if ((options.env ?? process.env).HITCH_API_KEY !== void 0) throw new AuthenticationError("HITCH_API_KEY is set — unset it to manage stored logins.");
871
+ function rejectEnvironmentCredentialManagement() {
872
+ if (process.env.HITCH_API_KEY !== void 0) throw new AuthenticationError("HITCH_API_KEY is set — unset it to manage stored logins.");
845
873
  }
846
874
  function requireAuth(auth) {
847
875
  if (auth === void 0) throw new NotLoggedInError("Not logged in. Run hitch login.");
848
876
  return auth;
849
877
  }
850
- function createStaticRouter(activeInstanceUrl, dependencies = {}, selectedOrg) {
851
- const fetcher = dependencies.fetch ?? globalThis.fetch;
852
- const now = dependencies.now ?? Date.now;
853
- const print = dependencies.print ?? console.log;
854
- const warn = dependencies.warn ?? console.error;
878
+ function createStaticRouter(activeInstanceUrl, selectedOrg) {
879
+ const fetcher = globalThis.fetch;
880
+ const print = console.log;
881
+ const warn = console.error;
855
882
  return os.router({
856
883
  login: os.meta({ description: "Sign in to a Hitch organization." }).input(z.object({
857
884
  apiKey: z.union([z.string(), z.boolean()]).default(false).describe("Store an API key, prompting when no value is given"),
858
885
  noBrowser: z.boolean().default(false).describe("Do not open a browser"),
859
886
  instance: z.string().optional().describe("Hitch instance URL")
860
887
  })).handler(async ({ input }) => {
861
- rejectEnvironmentCredentialManagement(dependencies.store);
888
+ rejectEnvironmentCredentialManagement();
862
889
  if (input.apiKey !== false && input.noBrowser) throw new AuthenticationError("--api-key and --no-browser cannot be used together.");
863
- const instanceUrl = input.instance === void 0 ? activeInstanceUrl : await resolveInstanceUrl(input.instance, dependencies.store);
890
+ const instanceUrl = input.instance === void 0 ? activeInstanceUrl : await resolveInstanceUrl(input.instance);
864
891
  let me;
865
892
  let membership;
866
893
  let storedCredentials;
867
894
  if (input.apiKey !== false) {
868
- const apiKey = typeof input.apiKey === "string" ? input.apiKey : await (dependencies.prompt ?? defaultPrompt)("Paste your API key: ");
895
+ const apiKey = typeof input.apiKey === "string" ? input.apiKey : await defaultPrompt("Paste your API key: ");
869
896
  if (apiKey.length === 0) throw new AuthenticationError("API key cannot be empty.");
870
897
  me = await fetchMe(fetcher, instanceUrl, {
871
898
  accessToken: apiKey,
@@ -884,37 +911,24 @@ function createStaticRouter(activeInstanceUrl, dependencies = {}, selectedOrg) {
884
911
  organizationId: organization.id,
885
912
  orgCode: organization.code
886
913
  };
887
- await setCredentials(instanceUrl, storedCredentials, dependencies.store);
914
+ await setCredentials(instanceUrl, storedCredentials);
888
915
  } else {
889
916
  const login = await loginWithDevice(instanceUrl, {
890
917
  expectedOrg: selectedOrg,
891
- fetch: fetcher,
892
- noBrowser: input.noBrowser,
893
- now,
894
- openUrl: dependencies.openUrl,
895
- print,
896
- sleep: dependencies.sleep,
897
- store: dependencies.store
918
+ noBrowser: input.noBrowser
898
919
  });
899
920
  me = login.me;
900
921
  membership = login.membership;
901
922
  storedCredentials = login.credentials;
902
923
  }
903
924
  const organizationUrl = organizationInstanceUrl(instanceUrl, storedCredentials.orgCode);
904
- await syncContractCache(organizationUrl, storedCredentials, {
905
- fetch: fetcher,
906
- store: dependencies.store
907
- });
908
- await syncAdminContractCache(organizationUrl, storedCredentials, {
909
- fetch: fetcher,
910
- store: dependencies.store,
911
- warn
912
- });
925
+ await syncContractCache(organizationUrl, storedCredentials);
926
+ await syncAdminContractCache(organizationUrl, storedCredentials);
913
927
  print(`Signed in as ${me.person.email} to ${membership.name}${storedCredentials.type === "apiKey" ? " (API key)" : ""}.`);
914
928
  }),
915
929
  logout: os.meta({ description: "Sign out and remove locally stored credentials." }).input(z.object({})).handler(async () => {
916
- rejectEnvironmentCredentialManagement(dependencies.store);
917
- const credentials = Object.values((await readCredentialStore(dependencies.store)).instances[normalizeInstanceUrl(activeInstanceUrl)] ?? {});
930
+ rejectEnvironmentCredentialManagement();
931
+ const credentials = Object.values((await readCredentialStore()).instances[normalizeInstanceUrl(activeInstanceUrl)] ?? {});
918
932
  await Promise.all(credentials.filter((credential) => credential.type === "session").map(async (credential) => {
919
933
  try {
920
934
  const response = await fetcher(`${activeInstanceUrl}/api/auth/sign-out`, {
@@ -927,18 +941,18 @@ function createStaticRouter(activeInstanceUrl, dependencies = {}, selectedOrg) {
927
941
  }
928
942
  }));
929
943
  const cacheUrls = [activeInstanceUrl, ...credentials.map((credential) => organizationInstanceUrl(activeInstanceUrl, credential.orgCode))];
930
- await Promise.all([deleteCredentials(activeInstanceUrl, void 0, dependencies.store), ...[...new Set(cacheUrls)].map((url) => deleteInstanceCache(url, dependencies.store))]);
944
+ await Promise.all([deleteCredentials(activeInstanceUrl), ...[...new Set(cacheUrls)].map((url) => deleteInstanceCache(url))]);
931
945
  print(`Logged out of ${activeInstanceUrl}.`);
932
946
  }),
933
947
  openapi: os.meta({ description: "Print the organization's OpenAPI spec as JSON (used by applet generate)" }).input(z.object({})).handler(async () => {
934
- const auth = await authWithOrganization(fetcher, activeInstanceUrl, requireAuth(await resolveAuth(activeInstanceUrl, dependencies.store, selectedOrg)), selectedOrg);
948
+ const auth = await authWithOrganization(fetcher, activeInstanceUrl, requireAuth(await resolveAuth(activeInstanceUrl, selectedOrg)), selectedOrg);
935
949
  const response = await fetcher(`${organizationInstanceUrl(activeInstanceUrl, auth.orgCode)}/api/v1/openapi.json`, { headers: { Authorization: `Bearer ${auth.accessToken}` } });
936
950
  rejectEnvironmentKey(response, auth);
937
951
  if (!response.ok) throw new AuthenticationError(`Failed to load the organization's OpenAPI spec (${response.status}).`);
938
952
  process.stdout.write(`${JSON.stringify(await response.json(), null, 2)}\n`);
939
953
  }),
940
954
  whoami: os.meta({ description: "Show the current Hitch identity and organization." }).input(z.object({})).handler(async () => {
941
- const auth = await authWithOrganization(fetcher, activeInstanceUrl, requireAuth(await resolveAuth(activeInstanceUrl, dependencies.store, selectedOrg)), selectedOrg);
955
+ const auth = await authWithOrganization(fetcher, activeInstanceUrl, requireAuth(await resolveAuth(activeInstanceUrl, selectedOrg)), selectedOrg);
942
956
  const me = await fetchMe(fetcher, organizationInstanceUrl(activeInstanceUrl, auth.orgCode), auth);
943
957
  const membership = me.memberships.find(({ orgId }) => orgId === auth.organizationId);
944
958
  if (membership === void 0) throw new AuthenticationError(`The current identity is not a member of organization "${auth.orgCode}".`);
@@ -951,57 +965,23 @@ function createStaticRouter(activeInstanceUrl, dependencies = {}, selectedOrg) {
951
965
  };
952
966
  }),
953
967
  sync: os.meta({ description: "Refresh the cached commands for the current organization." }).input(z.object({})).handler(async () => {
954
- const auth = await authWithOrganization(fetcher, activeInstanceUrl, requireAuth(await resolveAuth(activeInstanceUrl, dependencies.store, selectedOrg)), selectedOrg);
968
+ const auth = await authWithOrganization(fetcher, activeInstanceUrl, requireAuth(await resolveAuth(activeInstanceUrl, selectedOrg)), selectedOrg);
955
969
  const organizationUrl = organizationInstanceUrl(activeInstanceUrl, auth.orgCode);
956
- const count = await syncContractCache(organizationUrl, auth, {
957
- fetch: fetcher,
958
- store: dependencies.store
959
- });
960
- await syncAdminContractCache(organizationUrl, auth, {
961
- fetch: fetcher,
962
- store: dependencies.store,
963
- warn
964
- });
970
+ const count = await syncContractCache(organizationUrl, auth);
971
+ await syncAdminContractCache(organizationUrl, auth);
965
972
  print(`Synced ${count} object${count === 1 ? "" : "s"}.`);
966
973
  })
967
974
  });
968
975
  }
969
976
  //#endregion
970
977
  //#region src/organization.ts
971
- const ORG_CODE_PATTERN = /^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/;
972
- function readOrg(dir) {
973
- const path = resolve(dir, "package.json");
974
- let source;
975
- try {
976
- source = readFileSync(path, "utf8");
977
- } catch (error) {
978
- if (error instanceof Error && "code" in error && error.code === "ENOENT") return void 0;
979
- throw error;
980
- }
981
- let packageJson;
982
- try {
983
- packageJson = JSON.parse(source);
984
- } catch (error) {
985
- throw new Error(`Invalid JSON in ${path}.`, { cause: error });
986
- }
987
- if (packageJson === null || typeof packageJson !== "object" || !("hitch" in packageJson)) return;
988
- const hitch = packageJson.hitch;
989
- if (hitch === null || typeof hitch !== "object" || !("org" in hitch) || typeof hitch.org !== "string" || !ORG_CODE_PATTERN.test(hitch.org)) throw new Error(`Invalid "hitch.org" in ${path}.`);
990
- return hitch.org;
991
- }
992
- function readWorkspaceOrg(cwd) {
993
- const localOrg = readOrg(cwd);
994
- if (localOrg !== void 0) return localOrg;
995
- const appletsDir = dirname(cwd);
996
- return basename(appletsDir) === "applets" ? readOrg(dirname(appletsDir)) : void 0;
997
- }
998
978
  function validateOrg(orgCode, source) {
999
979
  if (!ORG_CODE_PATTERN.test(orgCode)) throw new Error(`Invalid organization in ${source}.`);
1000
980
  return orgCode;
1001
981
  }
1002
982
  function resolveOrganization(explicitOrg, cwd, env) {
1003
983
  const cliOrg = explicitOrg === void 0 ? void 0 : validateOrg(explicitOrg, "--org");
1004
- const workspaceOrg = readWorkspaceOrg(cwd);
984
+ const workspaceOrg = findWorkspaceConfig(cwd)?.org;
1005
985
  if (cliOrg !== void 0 && workspaceOrg !== void 0 && cliOrg !== workspaceOrg) throw new Error(`--org "${cliOrg}" does not match workspace organization "${workspaceOrg}".`);
1006
986
  if (cliOrg !== void 0 || workspaceOrg !== void 0) return cliOrg ?? workspaceOrg;
1007
987
  const defaultOrg = env.HITCH_DEFAULT_ORG;
@@ -1070,28 +1050,26 @@ async function loadContract(path, warn) {
1070
1050
  if (module.contract === null || typeof module.contract !== "object") throw new Error(`${path}: generated contract export is missing.`);
1071
1051
  return module.contract;
1072
1052
  }
1073
- async function buildRouter(dependencies = {}, options = {}) {
1074
- const instanceUrl = await resolveInstanceUrl(void 0, dependencies.store);
1075
- const selectedOrg = resolveOrganization(options.orgCode, process.cwd(), dependencies.store?.env ?? process.env);
1076
- const staticRouter = createStaticRouter(instanceUrl, dependencies, selectedOrg);
1053
+ async function buildRouter(options = {}) {
1054
+ const instanceUrl = await resolveInstanceUrl();
1055
+ const selectedOrg = resolveOrganization(options.orgCode, process.cwd(), process.env);
1056
+ const staticRouter = createStaticRouter(instanceUrl, selectedOrg);
1077
1057
  if (options.staticOnly) return staticRouter;
1078
1058
  let resolved;
1079
1059
  try {
1080
- resolved = await resolveAuth(instanceUrl, dependencies.store, selectedOrg);
1060
+ resolved = await resolveAuth(instanceUrl, selectedOrg);
1081
1061
  } catch (error) {
1082
- (dependencies.warn ?? console.error)(`hitch: ${error instanceof Error ? error.message : String(error)}`);
1062
+ console.error(`hitch: ${error instanceof Error ? error.message : String(error)}`);
1083
1063
  return staticRouter;
1084
1064
  }
1085
1065
  if (resolved === void 0) return staticRouter;
1086
- const auth = await authWithOrganization(dependencies.fetch ?? globalThis.fetch, instanceUrl, resolved, selectedOrg);
1087
- const warn = dependencies.warn ?? console.error;
1066
+ const auth = await authWithOrganization(globalThis.fetch, instanceUrl, resolved, selectedOrg);
1067
+ const warn = console.error;
1088
1068
  const organizationUrl = organizationInstanceUrl(instanceUrl, auth.orgCode);
1089
- const [contract, adminContract] = await Promise.all([loadContract(resolve(contractCacheDir(organizationUrl, auth.organizationId, dependencies.store), "orpc.gen.ts"), warn), loadContract(resolve(adminContractCacheDir(organizationUrl, auth.organizationId, dependencies.store), "orpc.gen.ts"), warn)]);
1069
+ const [contract, adminContract] = await Promise.all([loadContract(resolve(contractCacheDir(organizationUrl, auth.organizationId), "orpc.gen.ts"), warn), loadContract(resolve(adminContractCacheDir(organizationUrl, auth.organizationId), "orpc.gen.ts"), warn)]);
1090
1070
  const bridgeOptions = {
1091
1071
  instanceUrl: organizationUrl,
1092
- auth,
1093
- fetch: dependencies.fetch,
1094
- prompt: dependencies.prompt
1072
+ auth
1095
1073
  };
1096
1074
  const dynamicRouter = contract === void 0 ? {} : createBridgeRouter(contract, bridgeOptions);
1097
1075
  const staticNamespaces = /* @__PURE__ */ new Set([...Object.keys(staticRouter), "admin"]);
@@ -1110,10 +1088,10 @@ async function buildRouter(dependencies = {}, options = {}) {
1110
1088
  ...staticRouter
1111
1089
  });
1112
1090
  }
1113
- async function runCli(dependencies = {}, argv) {
1091
+ async function runCli(argv) {
1114
1092
  const selected = extractOrganizationArg(argv ?? process.argv.slice(2));
1115
1093
  const command = selected.argv.at(0);
1116
- const router = await buildRouter(dependencies, {
1094
+ const router = await buildRouter({
1117
1095
  orgCode: selected.orgCode,
1118
1096
  staticOnly: [
1119
1097
  "login",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hitch42/cli",
3
- "version": "0.1.1-beta.1",
3
+ "version": "0.1.1-beta.11",
4
4
  "license": "MIT",
5
5
  "bin": {
6
6
  "hitch": "./dist/hitch.js"
@@ -19,7 +19,7 @@
19
19
  "zod": "^4.4.3"
20
20
  },
21
21
  "engines": {
22
- "node": ">=22"
22
+ "node": ">=22.18"
23
23
  },
24
24
  "repository": {
25
25
  "type": "git",
@@ -29,6 +29,6 @@
29
29
  "publishConfig": {
30
30
  "access": "public",
31
31
  "registry": "https://registry.npmjs.org",
32
- "tag": "beta"
32
+ "tag": "latest"
33
33
  }
34
34
  }