@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.
- package/dist/hitch.js +114 -136
- 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(
|
|
59
|
-
const { credentialsPath } = storePaths(
|
|
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(
|
|
127
|
-
const env =
|
|
128
|
-
const home =
|
|
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
|
|
137
|
+
async function resolveInstanceUrl(override) {
|
|
138
138
|
if (override !== void 0) return normalizeInstanceUrl(override);
|
|
139
|
-
const envInstanceUrl =
|
|
139
|
+
const envInstanceUrl = process.env.HITCH_API_URL;
|
|
140
140
|
if (envInstanceUrl !== void 0 && envInstanceUrl.length > 0) return normalizeInstanceUrl(envInstanceUrl);
|
|
141
|
-
const { configPath } = storePaths(
|
|
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(
|
|
148
|
-
const store = await readRawCredentialStore(
|
|
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(
|
|
155
|
-
const apiKey =
|
|
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,
|
|
180
|
-
const auth = environmentAuth(
|
|
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(
|
|
184
|
-
const selectedOrg = orgCode ??
|
|
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,
|
|
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
|
|
199
|
-
const paths = storePaths(
|
|
200
|
-
const store = await readRawCredentialStore(
|
|
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
|
|
212
|
-
const paths = storePaths(
|
|
213
|
-
const store = await readRawCredentialStore(
|
|
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
|
|
236
|
-
return join(storePaths(
|
|
235
|
+
function instanceCacheDir(instanceUrl) {
|
|
236
|
+
return join(storePaths().cacheDir, instanceSlug(instanceUrl));
|
|
237
237
|
}
|
|
238
|
-
function contractCacheDir(instanceUrl, organizationId
|
|
239
|
-
return join(instanceCacheDir(instanceUrl
|
|
238
|
+
function contractCacheDir(instanceUrl, organizationId) {
|
|
239
|
+
return join(instanceCacheDir(instanceUrl), organizationId);
|
|
240
240
|
}
|
|
241
|
-
function adminContractCacheDir(instanceUrl, organizationId
|
|
242
|
-
return join(contractCacheDir(instanceUrl, organizationId
|
|
241
|
+
function adminContractCacheDir(instanceUrl, organizationId) {
|
|
242
|
+
return join(contractCacheDir(instanceUrl, organizationId), "admin");
|
|
243
243
|
}
|
|
244
|
-
async function deleteInstanceCache(instanceUrl
|
|
245
|
-
await rm(instanceCacheDir(instanceUrl
|
|
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 =
|
|
369
|
-
const 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
|
-
|
|
374
|
+
console.log(`Open ${verificationUrl} and confirm code ${device.user_code}.`);
|
|
375
375
|
if (!options.noBrowser) try {
|
|
376
|
-
|
|
376
|
+
defaultOpenUrl(verificationUrl);
|
|
377
377
|
} catch {}
|
|
378
|
-
const token = await pollForToken(fetcher, instanceUrl, device, now,
|
|
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
|
|
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
|
|
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 (
|
|
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
|
|
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\}
|
|
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)
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
827
|
+
const outDir = adminContractCacheDir(instanceUrl, credentials.organizationId);
|
|
800
828
|
let response;
|
|
801
829
|
try {
|
|
802
|
-
response = await
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
844
|
-
if (
|
|
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,
|
|
851
|
-
const fetcher =
|
|
852
|
-
const
|
|
853
|
-
const
|
|
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(
|
|
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
|
|
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
|
|
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
|
|
914
|
+
await setCredentials(instanceUrl, storedCredentials);
|
|
888
915
|
} else {
|
|
889
916
|
const login = await loginWithDevice(instanceUrl, {
|
|
890
917
|
expectedOrg: selectedOrg,
|
|
891
|
-
|
|
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
|
-
|
|
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(
|
|
917
|
-
const credentials = Object.values((await readCredentialStore(
|
|
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
|
|
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,
|
|
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,
|
|
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,
|
|
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
|
-
|
|
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 =
|
|
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(
|
|
1074
|
-
const instanceUrl = await resolveInstanceUrl(
|
|
1075
|
-
const selectedOrg = resolveOrganization(options.orgCode, process.cwd(),
|
|
1076
|
-
const staticRouter = createStaticRouter(instanceUrl,
|
|
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,
|
|
1060
|
+
resolved = await resolveAuth(instanceUrl, selectedOrg);
|
|
1081
1061
|
} catch (error) {
|
|
1082
|
-
|
|
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(
|
|
1087
|
-
const warn =
|
|
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
|
|
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(
|
|
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(
|
|
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.
|
|
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": "
|
|
32
|
+
"tag": "latest"
|
|
33
33
|
}
|
|
34
34
|
}
|