@hitch42/applet 0.1.1-beta.1
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/LICENSE +21 -0
- package/dist/_internal/client.d.ts +7 -0
- package/dist/_internal/client.js +60 -0
- package/dist/_internal/server.d.ts +79 -0
- package/dist/_internal/server.js +232 -0
- package/dist/applet.d.ts +1 -0
- package/dist/applet.js +160 -0
- package/dist/auth-DaVK0k5R.js +350 -0
- package/dist/batch-Qa4B4-Yp.d.ts +30 -0
- package/dist/build-CEku1lyb.js +2 -0
- package/dist/build-Rk3xDZV7.js +74 -0
- package/dist/client.d.ts +48 -0
- package/dist/client.js +38 -0
- package/dist/contract-DMrpbaLJ.d.ts +23 -0
- package/dist/deploy-D3BGDqgC.js +2416 -0
- package/dist/generate-6Nbt7uii.js +2 -0
- package/dist/generate-CH0VQmUi.js +484 -0
- package/dist/index.d.ts +74 -0
- package/dist/index.js +136 -0
- package/dist/init-BS1Y-eNL.js +240 -0
- package/dist/lazy-client-x0DCI98S.js +64 -0
- package/dist/load-config-BnxocLAQ.js +56 -0
- package/dist/logs-m00pzr_O.js +183 -0
- package/dist/manifest-BQCAxATS.d.ts +81 -0
- package/dist/manifest-DyoJ1MIL.js +174 -0
- package/dist/project-CxN-Dtc7.js +346 -0
- package/dist/react.d.ts +37 -0
- package/dist/react.js +517 -0
- package/dist/server.d.ts +46 -0
- package/dist/server.js +104 -0
- package/dist/upload-DRbVhuDm.js +114 -0
- package/dist/upload-YbRKEqR6.d.ts +112 -0
- package/dist/variables-CP7Ce3Np.js +133 -0
- package/dist/vite.d.ts +63 -0
- package/dist/vite.js +358 -0
- package/dist/workflow-CTo2kZF1.js +51 -0
- package/dist/workflow-DurM6Fwx.d.ts +28 -0
- package/package.json +104 -0
- package/scaffold/.agents/skills/hitch-applets/SKILL.md +137 -0
- package/scaffold/.agents/skills/hitch-applets/references/client.md +73 -0
- package/scaffold/.agents/skills/hitch-applets/references/files.md +109 -0
- package/scaffold/.agents/skills/hitch-applets/references/manifest.md +171 -0
- package/scaffold/.agents/skills/hitch-applets/references/markdown.md +37 -0
- package/scaffold/.agents/skills/hitch-applets/references/records.md +152 -0
- package/scaffold/.agents/skills/hitch-applets/references/schedules.md +86 -0
- package/scaffold/.agents/skills/hitch-applets/references/server.md +98 -0
- package/scaffold/.agents/skills/hitch-applets/references/workflows.md +73 -0
- package/scaffold/AGENTS.md +38 -0
- package/scaffold/applet/components.json +25 -0
- package/scaffold/applet/src/client/components/ui/alert.tsx +73 -0
- package/scaffold/applet/src/client/components/ui/avatar.tsx +100 -0
- package/scaffold/applet/src/client/components/ui/badge.tsx +55 -0
- package/scaffold/applet/src/client/components/ui/button.tsx +90 -0
- package/scaffold/applet/src/client/components/ui/card.tsx +85 -0
- package/scaffold/applet/src/client/components/ui/checkbox.tsx +35 -0
- package/scaffold/applet/src/client/components/ui/dialog.tsx +164 -0
- package/scaffold/applet/src/client/components/ui/dropdown-menu.tsx +235 -0
- package/scaffold/applet/src/client/components/ui/input-group.tsx +144 -0
- package/scaffold/applet/src/client/components/ui/input.tsx +22 -0
- package/scaffold/applet/src/client/components/ui/label.tsx +29 -0
- package/scaffold/applet/src/client/components/ui/popover.tsx +74 -0
- package/scaffold/applet/src/client/components/ui/select.tsx +243 -0
- package/scaffold/applet/src/client/components/ui/separator.tsx +25 -0
- package/scaffold/applet/src/client/components/ui/sheet.tsx +177 -0
- package/scaffold/applet/src/client/components/ui/skeleton.tsx +13 -0
- package/scaffold/applet/src/client/components/ui/spinner.tsx +16 -0
- package/scaffold/applet/src/client/components/ui/switch.tsx +41 -0
- package/scaffold/applet/src/client/components/ui/table.tsx +114 -0
- package/scaffold/applet/src/client/components/ui/tabs.tsx +78 -0
- package/scaffold/applet/src/client/components/ui/textarea.tsx +23 -0
- package/scaffold/applet/src/client/lib/utils.ts +6 -0
- package/scaffold/applet/src/client/styles/globals.css +136 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
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";
|
|
3
|
+
import { oc } from "@orpc/contract";
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
//#region src/hitch/contract.ts
|
|
6
|
+
function isObject(value) {
|
|
7
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
8
|
+
}
|
|
9
|
+
function pruneContract(contract, manifest) {
|
|
10
|
+
if (!isObject(contract) || !isObject(contract.records)) throw new Error("Hitch contract must contain a \"records\" router.");
|
|
11
|
+
const contractRecords = contract.records;
|
|
12
|
+
const records = Object.fromEntries(manifest.objects.map((manifestObject) => {
|
|
13
|
+
const procedures = contractRecords[manifestObject.code];
|
|
14
|
+
if (!isObject(procedures)) throw new Error(`Hitch contract object "${manifestObject.code}" is missing.`);
|
|
15
|
+
const pruned = { ...procedures };
|
|
16
|
+
if (!manifestObject.canCreate) delete pruned.create;
|
|
17
|
+
if (!manifestObject.canDelete) delete pruned.delete;
|
|
18
|
+
if (manifestObject.readWrite.length === 0) {
|
|
19
|
+
delete pruned.update;
|
|
20
|
+
delete pruned.updateWhere;
|
|
21
|
+
}
|
|
22
|
+
return [manifestObject.code, pruned];
|
|
23
|
+
}));
|
|
24
|
+
return Object.assign({}, contract, { records });
|
|
25
|
+
}
|
|
26
|
+
//#endregion
|
|
27
|
+
//#region src/hitch/files.ts
|
|
28
|
+
const fileFields = {
|
|
29
|
+
id: z.string(),
|
|
30
|
+
organizationId: z.string(),
|
|
31
|
+
key: z.string(),
|
|
32
|
+
filename: z.string(),
|
|
33
|
+
contentType: z.string(),
|
|
34
|
+
uploadedBy: z.string().nullable(),
|
|
35
|
+
createdAt: z.string(),
|
|
36
|
+
updatedAt: z.string()
|
|
37
|
+
};
|
|
38
|
+
const fileSchema = z.object({
|
|
39
|
+
...fileFields,
|
|
40
|
+
sizeBytes: z.number(),
|
|
41
|
+
arrivedAt: z.string()
|
|
42
|
+
});
|
|
43
|
+
const pendingFileSchema = z.object({
|
|
44
|
+
...fileFields,
|
|
45
|
+
sizeBytes: z.null(),
|
|
46
|
+
arrivedAt: z.null()
|
|
47
|
+
});
|
|
48
|
+
const createFileInputSchema = z.object({ body: z.object({
|
|
49
|
+
filename: z.string().trim().min(1),
|
|
50
|
+
contentType: z.string().trim().min(1)
|
|
51
|
+
}) });
|
|
52
|
+
const createFileOutputSchema = z.object({
|
|
53
|
+
data: pendingFileSchema,
|
|
54
|
+
uploadUrl: z.string()
|
|
55
|
+
});
|
|
56
|
+
const fileIdInputSchema = z.object({ params: z.object({ id: z.uuid() }) });
|
|
57
|
+
const getFileOutputSchema = z.object({
|
|
58
|
+
data: fileSchema,
|
|
59
|
+
downloadUrl: z.string()
|
|
60
|
+
});
|
|
61
|
+
const filesContract = {
|
|
62
|
+
create: oc.route({
|
|
63
|
+
method: "POST",
|
|
64
|
+
path: "/v1/files",
|
|
65
|
+
successStatus: 201,
|
|
66
|
+
inputStructure: "detailed"
|
|
67
|
+
}).input(createFileInputSchema).output(createFileOutputSchema),
|
|
68
|
+
confirm: oc.route({
|
|
69
|
+
method: "POST",
|
|
70
|
+
path: "/v1/files/{id}/confirm",
|
|
71
|
+
successStatus: 200,
|
|
72
|
+
inputStructure: "detailed"
|
|
73
|
+
}).input(fileIdInputSchema).output(getFileOutputSchema),
|
|
74
|
+
get: oc.route({
|
|
75
|
+
method: "GET",
|
|
76
|
+
path: "/v1/files/{id}",
|
|
77
|
+
inputStructure: "detailed"
|
|
78
|
+
}).input(fileIdInputSchema).output(getFileOutputSchema)
|
|
79
|
+
};
|
|
80
|
+
//#endregion
|
|
81
|
+
//#region src/hitch/me.ts
|
|
82
|
+
const personSchema = z.object({
|
|
83
|
+
id: z.uuid(),
|
|
84
|
+
firstName: z.string(),
|
|
85
|
+
lastName: z.string(),
|
|
86
|
+
email: z.email()
|
|
87
|
+
});
|
|
88
|
+
const membershipSchema = z.object({
|
|
89
|
+
orgId: z.uuid(),
|
|
90
|
+
code: z.string(),
|
|
91
|
+
name: z.string(),
|
|
92
|
+
iconFileId: z.uuid().nullable(),
|
|
93
|
+
role: z.enum(["member", "owner"]),
|
|
94
|
+
capabilities: z.array(z.string())
|
|
95
|
+
});
|
|
96
|
+
const meSchema = z.object({
|
|
97
|
+
person: personSchema,
|
|
98
|
+
memberships: z.array(membershipSchema),
|
|
99
|
+
organization: z.object({
|
|
100
|
+
id: z.uuid(),
|
|
101
|
+
code: z.string()
|
|
102
|
+
}).nullable(),
|
|
103
|
+
multiOrg: z.boolean()
|
|
104
|
+
});
|
|
105
|
+
const meContract = { get: oc.route({
|
|
106
|
+
method: "GET",
|
|
107
|
+
path: "/v1/me"
|
|
108
|
+
}).input(z.void()).output(meSchema) };
|
|
109
|
+
//#endregion
|
|
110
|
+
//#region src/hitch/teams.ts
|
|
111
|
+
const teamSchema = z.object({
|
|
112
|
+
id: z.uuid(),
|
|
113
|
+
name: z.string()
|
|
114
|
+
});
|
|
115
|
+
const teamMemberSchema = z.object({
|
|
116
|
+
userId: z.uuid(),
|
|
117
|
+
firstName: z.string(),
|
|
118
|
+
lastName: z.string(),
|
|
119
|
+
email: z.email()
|
|
120
|
+
});
|
|
121
|
+
const teamsOutputSchema = z.object({ data: z.array(teamSchema) });
|
|
122
|
+
const teamIdInputSchema = z.object({ params: z.object({ id: z.uuid() }) });
|
|
123
|
+
const teamMembersOutputSchema = z.object({ data: z.array(teamMemberSchema) });
|
|
124
|
+
const teamsContract = {
|
|
125
|
+
list: oc.route({
|
|
126
|
+
method: "GET",
|
|
127
|
+
path: "/v1/teams"
|
|
128
|
+
}).input(z.void()).output(teamsOutputSchema),
|
|
129
|
+
members: { list: oc.route({
|
|
130
|
+
method: "GET",
|
|
131
|
+
path: "/v1/teams/{id}/members",
|
|
132
|
+
inputStructure: "detailed"
|
|
133
|
+
}).input(teamIdInputSchema).output(teamMembersOutputSchema) }
|
|
134
|
+
};
|
|
135
|
+
//#endregion
|
|
136
|
+
export { appletDefinitionSchema, appletManifestSchema, appletSourceDefinitionSchema, createFileOutputSchema, fileSchema, filesContract, getFileOutputSchema, meContract, meSchema, pruneContract, registerHitchContract, teamMemberSchema, teamSchema, teamsContract, uploadFile };
|
|
@@ -0,0 +1,240 @@
|
|
|
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-CH0VQmUi.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-CxN-Dtc7.js";
|
|
6
|
+
import { access, cp, mkdir, readdir, writeFile } from "node:fs/promises";
|
|
7
|
+
import { relative, resolve } from "node:path";
|
|
8
|
+
import { spawn } from "node:child_process";
|
|
9
|
+
//#region src/init.ts
|
|
10
|
+
function isMissingFile(error) {
|
|
11
|
+
return error instanceof Error && "code" in error && error.code === "ENOENT";
|
|
12
|
+
}
|
|
13
|
+
async function assertTargetIsAvailable(paths, targetType) {
|
|
14
|
+
const existingPath = (await Promise.all(paths.map(async (path) => {
|
|
15
|
+
try {
|
|
16
|
+
await access(path);
|
|
17
|
+
return path;
|
|
18
|
+
} catch (error) {
|
|
19
|
+
if (isMissingFile(error)) return void 0;
|
|
20
|
+
throw error;
|
|
21
|
+
}
|
|
22
|
+
}))).find((path) => path !== void 0);
|
|
23
|
+
if (existingPath !== void 0) throw new Error(`Refusing to initialize ${targetType}: ${existingPath} already exists.`);
|
|
24
|
+
}
|
|
25
|
+
async function assertDirectoryIsEmpty(path) {
|
|
26
|
+
const [entry] = await readdir(path);
|
|
27
|
+
if (entry !== void 0) throw new Error(`Refusing to initialize a workspace: ${path} is not empty (${entry} exists).`);
|
|
28
|
+
}
|
|
29
|
+
async function installDependencies(target, install) {
|
|
30
|
+
if (!install) return "skipped";
|
|
31
|
+
console.log("Installing dependencies with pnpm install...");
|
|
32
|
+
return new Promise((resolveInstall, rejectInstall) => {
|
|
33
|
+
let finished = false;
|
|
34
|
+
const child = spawn("pnpm", ["install"], {
|
|
35
|
+
cwd: target,
|
|
36
|
+
stdio: "inherit"
|
|
37
|
+
});
|
|
38
|
+
child.once("error", (error) => {
|
|
39
|
+
finished = true;
|
|
40
|
+
if (isMissingFile(error)) {
|
|
41
|
+
console.error("pnpm was not found. Install pnpm, then run pnpm install.");
|
|
42
|
+
resolveInstall("failed");
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
rejectInstall(error);
|
|
46
|
+
});
|
|
47
|
+
child.once("close", (code) => {
|
|
48
|
+
if (finished) return;
|
|
49
|
+
finished = true;
|
|
50
|
+
if (code === 0) {
|
|
51
|
+
resolveInstall("succeeded");
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
console.error(`pnpm install failed${code === null ? "." : ` with exit code ${code}.`}`);
|
|
55
|
+
resolveInstall("failed");
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
async function runGit(cwd, args) {
|
|
60
|
+
const env = Object.fromEntries(Object.entries(process.env).filter(([key]) => ![
|
|
61
|
+
"GIT_COMMON_DIR",
|
|
62
|
+
"GIT_DIR",
|
|
63
|
+
"GIT_INDEX_FILE",
|
|
64
|
+
"GIT_WORK_TREE"
|
|
65
|
+
].includes(key)));
|
|
66
|
+
return new Promise((resolveGit, rejectGit) => {
|
|
67
|
+
let finished = false;
|
|
68
|
+
const child = spawn("git", args, {
|
|
69
|
+
cwd,
|
|
70
|
+
env,
|
|
71
|
+
stdio: [
|
|
72
|
+
"ignore",
|
|
73
|
+
"ignore",
|
|
74
|
+
"inherit"
|
|
75
|
+
]
|
|
76
|
+
});
|
|
77
|
+
child.once("error", (error) => {
|
|
78
|
+
finished = true;
|
|
79
|
+
if (isMissingFile(error)) {
|
|
80
|
+
console.error("git was not found; skipping repository setup.");
|
|
81
|
+
resolveGit(false);
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
rejectGit(error);
|
|
85
|
+
});
|
|
86
|
+
child.once("close", (code) => {
|
|
87
|
+
if (finished) return;
|
|
88
|
+
finished = true;
|
|
89
|
+
if (code !== 0) console.error(`git ${args[0]} failed${code === null ? "." : ` with exit code ${code}.`}`);
|
|
90
|
+
resolveGit(code === 0);
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
async function generateAppletFiles(target, org, options, installResult) {
|
|
95
|
+
if (installResult === "failed") return false;
|
|
96
|
+
const previousCwd = process.cwd();
|
|
97
|
+
try {
|
|
98
|
+
const instanceUrl = await resolveInstanceUrl(void 0, options.store);
|
|
99
|
+
if (await resolveAuthForOrg(instanceUrl, org, options.store) === void 0) {
|
|
100
|
+
console.log("Not logged in. Run applet generate to pull the organization's generated files; it will prompt a login.");
|
|
101
|
+
return false;
|
|
102
|
+
}
|
|
103
|
+
process.chdir(target);
|
|
104
|
+
await runGenerate({
|
|
105
|
+
fetch: options.fetch,
|
|
106
|
+
login: options.login,
|
|
107
|
+
stdinIsTTY: options.stdinIsTTY,
|
|
108
|
+
store: options.store
|
|
109
|
+
});
|
|
110
|
+
return true;
|
|
111
|
+
} catch (error) {
|
|
112
|
+
console.warn(`Generation skipped: ${error instanceof Error ? error.message : String(error)}`);
|
|
113
|
+
console.warn("Run applet generate in the applet directory after you resolve this problem.");
|
|
114
|
+
return false;
|
|
115
|
+
} finally {
|
|
116
|
+
process.chdir(previousCwd);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
async function runInit(org, options = {}) {
|
|
120
|
+
if (!ORG_CODE_PATTERN.test(org)) throw new Error(`Invalid organization code "${org}".`);
|
|
121
|
+
const target = process.cwd();
|
|
122
|
+
await assertDirectoryIsEmpty(target);
|
|
123
|
+
const cli = await loadCliManifest();
|
|
124
|
+
if (cli === void 0) throw new Error("The CLI package manifest is not published.");
|
|
125
|
+
const config = {
|
|
126
|
+
sdk: `${cli.name}@${cli.version}`,
|
|
127
|
+
org
|
|
128
|
+
};
|
|
129
|
+
await writeFile(resolve(target, "hitch.config.ts"), `import type { HitchConfig } from "@hitch42/applet";
|
|
130
|
+
|
|
131
|
+
// After a clone, run: pnpm dlx ${config.sdk} bootstrap
|
|
132
|
+
export default {
|
|
133
|
+
sdk: ${JSON.stringify(config.sdk)},
|
|
134
|
+
org: ${JSON.stringify(config.org)},
|
|
135
|
+
} satisfies HitchConfig;
|
|
136
|
+
`);
|
|
137
|
+
await writeWorkspaceSupport(target, config, cli);
|
|
138
|
+
const initialized = (options.git ?? true) && await runGit(target, [
|
|
139
|
+
"init",
|
|
140
|
+
"-b",
|
|
141
|
+
"main"
|
|
142
|
+
]);
|
|
143
|
+
const installResult = await installDependencies(target, options.install ?? true);
|
|
144
|
+
if (initialized) {
|
|
145
|
+
if ((options.commit ?? true) && await runGit(target, ["add", "-A"]) && await runGit(target, [
|
|
146
|
+
"commit",
|
|
147
|
+
"-m",
|
|
148
|
+
`applet init ${org}`
|
|
149
|
+
])) console.log("Created a git repository with an initial commit.");
|
|
150
|
+
else console.log("Created a git repository.");
|
|
151
|
+
}
|
|
152
|
+
const nextSteps = [...installResult === "succeeded" ? [] : ["pnpm install"], "pnpm exec applet new <code>"];
|
|
153
|
+
console.log(`Workspace created in ${target}.\n\nNext steps:\n${nextSteps.map((step) => ` ${step}`).join("\n")}`);
|
|
154
|
+
}
|
|
155
|
+
async function runBootstrap(options = {}) {
|
|
156
|
+
const target = process.cwd();
|
|
157
|
+
const config = readWorkspaceConfig(target);
|
|
158
|
+
if (config === void 0) throw new Error(`Not a Hitch workspace root: ${target}. Run applet bootstrap from the directory that contains hitch.config.ts.`);
|
|
159
|
+
const cli = await loadCliManifest();
|
|
160
|
+
if (cli === void 0) throw new Error("The CLI package manifest is not published.");
|
|
161
|
+
assertWorkspaceCli(config, cli);
|
|
162
|
+
const applets = await readdir(resolve(target, "applets"), { withFileTypes: true }).catch((error) => {
|
|
163
|
+
if (isMissingFile(error)) return [];
|
|
164
|
+
throw error;
|
|
165
|
+
});
|
|
166
|
+
const definitions = await Promise.all(applets.filter((entry) => entry.isDirectory()).map(async ({ name }) => {
|
|
167
|
+
const root = resolve(target, "applets", name);
|
|
168
|
+
return {
|
|
169
|
+
definition: await loadAppletSourceDefinition(root),
|
|
170
|
+
root
|
|
171
|
+
};
|
|
172
|
+
}));
|
|
173
|
+
await Promise.all([writeWorkspaceSupport(target, config, cli), ...definitions.map(({ definition, root }) => writeAppletSupport(root, definition))]);
|
|
174
|
+
const installResult = await installDependencies(target, options.install ?? true);
|
|
175
|
+
console.log(`Workspace files generated in ${target}.${installResult === "failed" ? "\n\nNext step:\n pnpm install" : ""}`);
|
|
176
|
+
}
|
|
177
|
+
async function runNew(options) {
|
|
178
|
+
if (!APPLET_CODE_PATTERN.test(options.code)) throw new Error(`Invalid applet code "${options.code}".`);
|
|
179
|
+
if (options.clientOnly === true && options.serverOnly === true) throw new Error("Use only one of --client-only or --server-only.");
|
|
180
|
+
const workspaceRoot = process.cwd();
|
|
181
|
+
const name = options.code.replaceAll("_", " ").replace(/^./, (character) => character.toUpperCase());
|
|
182
|
+
const config = readWorkspaceConfig(workspaceRoot);
|
|
183
|
+
if (config === void 0) throw new Error(`Not a Hitch workspace root: ${workspaceRoot}. Run applet new from the directory that contains hitch.config.ts.`);
|
|
184
|
+
const cli = await loadCliManifest({ allowDevelopment: true });
|
|
185
|
+
if (cli !== void 0) assertWorkspaceCli(config, cli);
|
|
186
|
+
const appletTarget = resolve(workspaceRoot, "applets", options.code);
|
|
187
|
+
await assertTargetIsAvailable([resolve(appletTarget, "applet.manifest.ts"), resolve(appletTarget, "src")], "an applet");
|
|
188
|
+
const hasClient = options.serverOnly !== true;
|
|
189
|
+
const hasServer = options.clientOnly !== true;
|
|
190
|
+
if (hasClient) await cp(new URL("../scaffold/applet", import.meta.url), appletTarget, { recursive: true });
|
|
191
|
+
await Promise.all([hasClient ? mkdir(resolve(appletTarget, "src/client/routes"), { recursive: true }) : void 0, hasServer ? mkdir(resolve(appletTarget, "src/server"), { recursive: true }) : void 0]);
|
|
192
|
+
const definition = {
|
|
193
|
+
name,
|
|
194
|
+
code: options.code,
|
|
195
|
+
objects: [],
|
|
196
|
+
dependencies: {},
|
|
197
|
+
devDependencies: {}
|
|
198
|
+
};
|
|
199
|
+
await Promise.all([
|
|
200
|
+
writeFile(resolve(appletTarget, "applet.manifest.ts"), `import type { AppletSourceDefinition } from "@hitch42/applet";
|
|
201
|
+
|
|
202
|
+
export default {
|
|
203
|
+
name: "${name}",
|
|
204
|
+
code: "${options.code}",
|
|
205
|
+
objects: [],
|
|
206
|
+
dependencies: {},
|
|
207
|
+
devDependencies: {},
|
|
208
|
+
} as const satisfies AppletSourceDefinition;
|
|
209
|
+
`),
|
|
210
|
+
hasClient ? writeFile(resolve(appletTarget, "src/client/routes/index.tsx"), `import { createFileRoute } from "@tanstack/react-router";
|
|
211
|
+
|
|
212
|
+
import "@/client/styles/globals.css";
|
|
213
|
+
|
|
214
|
+
export const Route = createFileRoute("/${options.code}/")({
|
|
215
|
+
component: () => <p>Hello from ${options.code}</p>,
|
|
216
|
+
});
|
|
217
|
+
`) : void 0,
|
|
218
|
+
hasServer ? writeFile(resolve(appletTarget, "src/server/main.ts"), `import { os, type } from "@orpc/server";
|
|
219
|
+
|
|
220
|
+
// Delete this file if you do not require an API.
|
|
221
|
+
const router = {
|
|
222
|
+
hello: os.input(type<{ name: string }>()).handler(({ input }) => \`Hello, \${input.name}!\`),
|
|
223
|
+
};
|
|
224
|
+
|
|
225
|
+
export default { router };
|
|
226
|
+
`) : void 0
|
|
227
|
+
]);
|
|
228
|
+
await Promise.all([writeAppletSupport(appletTarget, definition), cli === void 0 ? void 0 : writeWorkspaceSupport(workspaceRoot, config, cli)]);
|
|
229
|
+
const installResult = await installDependencies(workspaceRoot, options.install ?? true);
|
|
230
|
+
const generateSucceeded = await generateAppletFiles(appletTarget, config.org, options, installResult);
|
|
231
|
+
const relativeTarget = relative(workspaceRoot, appletTarget);
|
|
232
|
+
const nextSteps = [
|
|
233
|
+
...installResult === "succeeded" ? [`cd ${JSON.stringify(relativeTarget)}`] : ["pnpm install", `cd ${JSON.stringify(relativeTarget)}`],
|
|
234
|
+
...generateSucceeded ? [] : ["pnpm exec applet generate"],
|
|
235
|
+
"pnpm exec vite"
|
|
236
|
+
];
|
|
237
|
+
console.log(`Applet created in ${relativeTarget}.\n\nNext steps:\n${nextSteps.map((step) => ` ${step}`).join("\n")}`);
|
|
238
|
+
}
|
|
239
|
+
//#endregion
|
|
240
|
+
export { runBootstrap, runInit, runNew };
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { isContractProcedure } from "@orpc/contract";
|
|
2
|
+
//#region src/hitch/batch.ts
|
|
3
|
+
const BATCH_VERBS = [
|
|
4
|
+
"create",
|
|
5
|
+
"update",
|
|
6
|
+
"delete",
|
|
7
|
+
"updateWhere"
|
|
8
|
+
];
|
|
9
|
+
function withBatchRecorder(target, contract) {
|
|
10
|
+
if (isContractProcedure(contract)) return target;
|
|
11
|
+
const batchContract = Reflect.get(contract, "batch");
|
|
12
|
+
if (!isContractProcedure(batchContract)) return target;
|
|
13
|
+
return new Proxy(target, { get(inner, property, receiver) {
|
|
14
|
+
const value = Reflect.get(inner, property, receiver);
|
|
15
|
+
if (property !== "batch") return value;
|
|
16
|
+
return (...args) => {
|
|
17
|
+
const [input, options] = args;
|
|
18
|
+
if (typeof input !== "function") return Reflect.apply(value, receiver, args);
|
|
19
|
+
return (async () => {
|
|
20
|
+
const requests = [];
|
|
21
|
+
let active = true;
|
|
22
|
+
const records = Reflect.get(contract, "records");
|
|
23
|
+
const tx = Object.fromEntries(Object.entries(records).map(([code, procedures]) => [code, Object.fromEntries(BATCH_VERBS.filter((verb) => typeof procedures === "object" && procedures !== null && isContractProcedure(Reflect.get(procedures, verb))).map((verb) => [verb, (procedureInput) => {
|
|
24
|
+
if (!active) throw new Error("The batch was already sent.");
|
|
25
|
+
const body = Reflect.get(procedureInput, "body");
|
|
26
|
+
const id = verb === "update" || verb === "delete" ? Reflect.get(Reflect.get(procedureInput, "params"), "id") : void 0;
|
|
27
|
+
requests.push({
|
|
28
|
+
method: verb === "create" || verb === "updateWhere" ? "POST" : verb === "update" ? "PATCH" : "DELETE",
|
|
29
|
+
path: verb === "create" ? `/v1/${code}` : verb === "updateWhere" ? `/v1/${code}/update` : `/v1/${code}/${String(id)}`,
|
|
30
|
+
...verb === "delete" ? {} : { body }
|
|
31
|
+
});
|
|
32
|
+
}]))]));
|
|
33
|
+
try {
|
|
34
|
+
await Reflect.apply(input, void 0, [tx]);
|
|
35
|
+
} finally {
|
|
36
|
+
active = false;
|
|
37
|
+
}
|
|
38
|
+
const recorderOptions = options;
|
|
39
|
+
return Reflect.apply(value, receiver, [{
|
|
40
|
+
body: {
|
|
41
|
+
requests,
|
|
42
|
+
...recorderOptions?.dryRun ? { dry_run: true } : {}
|
|
43
|
+
},
|
|
44
|
+
...recorderOptions?.idempotencyKey ? { headers: { "Idempotency-Key": recorderOptions.idempotencyKey } } : {}
|
|
45
|
+
}]);
|
|
46
|
+
})();
|
|
47
|
+
};
|
|
48
|
+
} });
|
|
49
|
+
}
|
|
50
|
+
//#endregion
|
|
51
|
+
//#region src/lazy-client.ts
|
|
52
|
+
function createLazyClientProxy(build) {
|
|
53
|
+
let client;
|
|
54
|
+
const at = (path) => new Proxy(() => void 0, {
|
|
55
|
+
get: (_target, property) => at([...path, property]),
|
|
56
|
+
apply: (_target, _thisArg, args) => {
|
|
57
|
+
client ??= build();
|
|
58
|
+
return path.reduce((value, property) => value[property], client)(...args);
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
return at([]);
|
|
62
|
+
}
|
|
63
|
+
//#endregion
|
|
64
|
+
export { withBatchRecorder as n, createLazyClientProxy as t };
|
|
@@ -0,0 +1,56 @@
|
|
|
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 };
|
|
@@ -0,0 +1,183 @@
|
|
|
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";
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
//#region src/logs.ts
|
|
6
|
+
const LOG_LEVELS = [
|
|
7
|
+
"debug",
|
|
8
|
+
"log",
|
|
9
|
+
"info",
|
|
10
|
+
"warn",
|
|
11
|
+
"error"
|
|
12
|
+
];
|
|
13
|
+
const MAX_RANGE_MS = 6048e5;
|
|
14
|
+
const LIVE_OVERLAP_MS = 1e4;
|
|
15
|
+
const POLL_INTERVAL_MS = 2e3;
|
|
16
|
+
const MAX_SEEN_IDS = 5e3;
|
|
17
|
+
const logSchema = z.object({
|
|
18
|
+
id: z.string(),
|
|
19
|
+
timestamp: z.number(),
|
|
20
|
+
level: z.string(),
|
|
21
|
+
message: z.string(),
|
|
22
|
+
releaseId: z.string(),
|
|
23
|
+
type: z.string().optional(),
|
|
24
|
+
runId: z.string().optional(),
|
|
25
|
+
workflow: z.string().optional(),
|
|
26
|
+
invocation: z.number().optional(),
|
|
27
|
+
outcome: z.string().optional(),
|
|
28
|
+
eventType: z.string().optional(),
|
|
29
|
+
statusCode: z.number().optional(),
|
|
30
|
+
cpuTimeMs: z.number().optional(),
|
|
31
|
+
wallTimeMs: z.number().optional()
|
|
32
|
+
});
|
|
33
|
+
const logsResponseSchema = z.object({ data: z.array(logSchema) });
|
|
34
|
+
function parseSince(value = "1h") {
|
|
35
|
+
const match = /^(\d+)(s|m|h|d)$/.exec(value);
|
|
36
|
+
if (!match) throw new Error("--since must use a duration such as \"30m\", \"1h\", or \"2d\".");
|
|
37
|
+
const duration = Number(match[1]) * {
|
|
38
|
+
s: 1e3,
|
|
39
|
+
m: 6e4,
|
|
40
|
+
h: 36e5,
|
|
41
|
+
d: 864e5
|
|
42
|
+
}[match[2]];
|
|
43
|
+
if (!Number.isSafeInteger(duration) || duration < 1e3 || duration > MAX_RANGE_MS) throw new Error("--since must be between 1s and 7d.");
|
|
44
|
+
return duration;
|
|
45
|
+
}
|
|
46
|
+
function parseLimit(value = "100") {
|
|
47
|
+
const limit = Number(value);
|
|
48
|
+
if (!Number.isInteger(limit) || limit < 1 || limit > 100) throw new Error("--limit must be an integer from 1 to 100.");
|
|
49
|
+
return limit;
|
|
50
|
+
}
|
|
51
|
+
function parseLevel(value) {
|
|
52
|
+
if (value === void 0) return void 0;
|
|
53
|
+
const parsed = z.enum(LOG_LEVELS).safeParse(value);
|
|
54
|
+
if (!parsed.success) throw new Error("--level must be debug, log, info, warn, or error.");
|
|
55
|
+
return parsed.data;
|
|
56
|
+
}
|
|
57
|
+
function parseFilter(value) {
|
|
58
|
+
if (value === void 0) return void 0;
|
|
59
|
+
try {
|
|
60
|
+
const filter = JSON.parse(value);
|
|
61
|
+
if (filter === null || typeof filter !== "object" || Array.isArray(filter)) throw new Error();
|
|
62
|
+
return filter;
|
|
63
|
+
} catch {
|
|
64
|
+
throw new Error("--filter must be a JSON object.");
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
function parseSearch(value) {
|
|
68
|
+
if (value === void 0) return void 0;
|
|
69
|
+
const search = value.trim();
|
|
70
|
+
if (search.length === 0 || search.length > 1e3) throw new Error("--search must contain from 1 to 1000 characters.");
|
|
71
|
+
return search;
|
|
72
|
+
}
|
|
73
|
+
async function resolveTarget(code) {
|
|
74
|
+
if (code === void 0) {
|
|
75
|
+
const manifest = await loadAppletConfig(process.cwd());
|
|
76
|
+
return {
|
|
77
|
+
code: manifest.code,
|
|
78
|
+
org: manifest.org
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
if (!APPLET_CODE_PATTERN.test(code)) throw new Error(`Invalid applet code "${code}".`);
|
|
82
|
+
return {
|
|
83
|
+
code,
|
|
84
|
+
org: readWorkspaceConfig(process.cwd())?.org ?? resolveWorkspaceOrg(process.cwd())
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
async function readErrorDetails(response) {
|
|
88
|
+
try {
|
|
89
|
+
const body = await response.json();
|
|
90
|
+
if (body !== null && typeof body === "object") return ["code" in body && typeof body.code === "string" ? body.code : void 0, "message" in body && typeof body.message === "string" ? body.message : void 0].filter((value) => value !== void 0).join(": ");
|
|
91
|
+
} catch {
|
|
92
|
+
return "";
|
|
93
|
+
}
|
|
94
|
+
return "";
|
|
95
|
+
}
|
|
96
|
+
function waitForPoll(signal) {
|
|
97
|
+
if (signal?.aborted === true) return Promise.resolve(false);
|
|
98
|
+
return new Promise((resolve) => {
|
|
99
|
+
const timeout = setTimeout(() => {
|
|
100
|
+
signal?.removeEventListener("abort", onAbort);
|
|
101
|
+
resolve(true);
|
|
102
|
+
}, POLL_INTERVAL_MS);
|
|
103
|
+
const onAbort = () => {
|
|
104
|
+
clearTimeout(timeout);
|
|
105
|
+
resolve(false);
|
|
106
|
+
};
|
|
107
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
function printLog(log) {
|
|
111
|
+
const details = log.type === "invocation" ? [
|
|
112
|
+
log.statusCode === void 0 ? void 0 : `status=${log.statusCode}`,
|
|
113
|
+
log.outcome === void 0 ? void 0 : `outcome=${log.outcome}`,
|
|
114
|
+
log.wallTimeMs === void 0 ? void 0 : `wall=${log.wallTimeMs}ms`,
|
|
115
|
+
log.cpuTimeMs === void 0 ? void 0 : `cpu=${log.cpuTimeMs}ms`
|
|
116
|
+
].filter((value) => value !== void 0) : [];
|
|
117
|
+
console.log(`${new Date(log.timestamp).toISOString()} ${log.level.toUpperCase().padEnd(5)} ${log.type === "workflow" ? `[${log.workflow} ${log.runId?.slice(0, 8)}#${log.invocation}] ` : ""}${log.message}${details.length === 0 ? "" : ` [${details.join(" ")}]`}`);
|
|
118
|
+
}
|
|
119
|
+
async function runLogs(options) {
|
|
120
|
+
const [{ code, org }, since, limit, level, userFilter, search] = await Promise.all([
|
|
121
|
+
resolveTarget(options.code),
|
|
122
|
+
Promise.resolve(parseSince(options.since)),
|
|
123
|
+
Promise.resolve(parseLimit(options.limit)),
|
|
124
|
+
Promise.resolve(parseLevel(options.level)),
|
|
125
|
+
Promise.resolve(parseFilter(options.filter)),
|
|
126
|
+
Promise.resolve(parseSearch(options.search))
|
|
127
|
+
]);
|
|
128
|
+
const runFilter = options.run === void 0 ? void 0 : { runId: { $eq: options.run } };
|
|
129
|
+
const filter = runFilter === void 0 ? userFilter : userFilter === void 0 ? runFilter : { $and: [runFilter, userFilter] };
|
|
130
|
+
const fetcher = options.fetch ?? globalThis.fetch;
|
|
131
|
+
const startedAt = Date.now();
|
|
132
|
+
const requestLogs = async (from, to) => {
|
|
133
|
+
const response = await fetchWithOrgAuth(org, options, (instanceUrl, accessToken) => {
|
|
134
|
+
const url = new URL(`/api/v1/applets/${encodeURIComponent(code)}/logs/query`, `${instanceUrl}/`);
|
|
135
|
+
const levelFilter = level === void 0 ? void 0 : { level: { $in: LOG_LEVELS.slice(LOG_LEVELS.indexOf(level)) } };
|
|
136
|
+
return fetcher(url, {
|
|
137
|
+
method: "POST",
|
|
138
|
+
headers: {
|
|
139
|
+
Authorization: `Bearer ${accessToken}`,
|
|
140
|
+
"content-type": "application/json"
|
|
141
|
+
},
|
|
142
|
+
body: JSON.stringify({
|
|
143
|
+
from,
|
|
144
|
+
to,
|
|
145
|
+
limit,
|
|
146
|
+
...search === void 0 ? {} : { search },
|
|
147
|
+
...levelFilter === void 0 && filter === void 0 ? {} : { filter: levelFilter === void 0 ? filter : filter === void 0 ? levelFilter : { $and: [levelFilter, filter] } }
|
|
148
|
+
})
|
|
149
|
+
});
|
|
150
|
+
});
|
|
151
|
+
if (!response.ok) {
|
|
152
|
+
const details = await readErrorDetails(response);
|
|
153
|
+
throw new Error(`Logs request failed (${response.status})${details ? `: ${details}` : ""}.`);
|
|
154
|
+
}
|
|
155
|
+
return logsResponseSchema.parse(await response.json()).data;
|
|
156
|
+
};
|
|
157
|
+
const seenIds = /* @__PURE__ */ new Set();
|
|
158
|
+
const seenIdQueue = [];
|
|
159
|
+
const printNewLogs = (logs) => {
|
|
160
|
+
logs.forEach((log) => {
|
|
161
|
+
if (seenIds.has(log.id)) return;
|
|
162
|
+
seenIds.add(log.id);
|
|
163
|
+
seenIdQueue.push(log.id);
|
|
164
|
+
if (seenIdQueue.length > MAX_SEEN_IDS) seenIds.delete(seenIdQueue.shift());
|
|
165
|
+
printLog(log);
|
|
166
|
+
});
|
|
167
|
+
};
|
|
168
|
+
const initialLogs = await requestLogs(startedAt - since, startedAt);
|
|
169
|
+
printNewLogs(initialLogs);
|
|
170
|
+
if (options.live !== true) {
|
|
171
|
+
if (initialLogs.length === 0) console.log(`No logs found for "${code}".`);
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
let lastTimestamp = initialLogs.at(-1)?.timestamp ?? startedAt;
|
|
175
|
+
while (await waitForPoll(options.signal)) {
|
|
176
|
+
const to = Date.now();
|
|
177
|
+
const logs = await requestLogs(Math.max(startedAt - since, lastTimestamp - LIVE_OVERLAP_MS), to);
|
|
178
|
+
printNewLogs(logs);
|
|
179
|
+
lastTimestamp = Math.max(lastTimestamp, ...logs.map(({ timestamp }) => timestamp));
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
//#endregion
|
|
183
|
+
export { runLogs };
|