@hue-run/sdk 0.3.2 → 0.4.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/CLI.md +270 -47
- package/ENVIRONMENTS.md +10 -0
- package/README.md +19 -3
- package/dist/client.d.ts +5 -5
- package/dist/client.js +13 -6
- package/dist/environment/tools.d.ts +6 -1
- package/dist/environment/tools.js +7 -1
- package/dist/environment/types.d.ts +6 -1
- package/dist/receipt.js +36 -8
- package/dist/setup/application.d.ts +74 -0
- package/dist/setup/application.js +766 -0
- package/dist/setup/backend.d.ts +229 -0
- package/dist/setup/backend.js +855 -0
- package/dist/setup/checkpoint.js +100 -30
- package/dist/setup/cli.js +20 -4
- package/dist/setup/configure.d.ts +13 -0
- package/dist/setup/configure.js +454 -0
- package/dist/setup/credential.d.ts +2 -0
- package/dist/setup/credential.js +9 -0
- package/dist/setup/detect.js +4 -1
- package/dist/setup/installation.d.ts +118 -0
- package/dist/setup/installation.js +605 -0
- package/dist/setup/lock.d.ts +2 -0
- package/dist/setup/lock.js +38 -0
- package/dist/setup/machine.d.ts +1 -10
- package/dist/setup/machine.js +8 -7
- package/dist/setup/render.d.ts +3 -1
- package/dist/setup/render.js +209 -6
- package/dist/setup/runner.d.ts +26 -76
- package/dist/setup/runner.js +320 -45
- package/dist/setup/socket.d.ts +7 -0
- package/dist/setup/socket.js +144 -0
- package/dist/setup/source.d.ts +9 -0
- package/dist/setup/source.js +269 -0
- package/dist/setup/types.d.ts +16 -9
- package/dist/setup/types.js +1 -1
- package/dist/setup.d.ts +6 -2
- package/dist/setup.js +3 -0
- package/dist/types.d.ts +24 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +2 -1
- package/setup-events.schema.json +16 -9
package/dist/setup/checkpoint.js
CHANGED
|
@@ -1,14 +1,82 @@
|
|
|
1
1
|
import { createHash, randomUUID } from "node:crypto";
|
|
2
2
|
import { constants } from "node:fs";
|
|
3
|
-
import {
|
|
3
|
+
import { lstat, mkdir, open, realpath, rename, unlink } from "node:fs/promises";
|
|
4
4
|
import { homedir, platform } from "node:os";
|
|
5
5
|
import { dirname, isAbsolute, join, relative, resolve } from "node:path";
|
|
6
6
|
const MAX_CHECKPOINT_BYTES = 256 * 1024;
|
|
7
|
-
const STEPS = [
|
|
7
|
+
const STEPS = [
|
|
8
|
+
"detect-project",
|
|
9
|
+
"install-runtime",
|
|
10
|
+
"configure-telemetry",
|
|
11
|
+
"verify-application-receipt",
|
|
12
|
+
"claim-project",
|
|
13
|
+
];
|
|
14
|
+
const LEGACY_STEPS = [
|
|
15
|
+
"detect-project",
|
|
16
|
+
"configure-telemetry",
|
|
17
|
+
"verify-receipt",
|
|
18
|
+
"claim-project",
|
|
19
|
+
];
|
|
8
20
|
function isInside(parent, child) {
|
|
9
21
|
const path = relative(parent, child);
|
|
10
22
|
return path === "" || (!path.startsWith("..") && !isAbsolute(path));
|
|
11
23
|
}
|
|
24
|
+
async function cleanupCheckpointTemporary(path) {
|
|
25
|
+
try {
|
|
26
|
+
await unlink(path);
|
|
27
|
+
}
|
|
28
|
+
catch (error) {
|
|
29
|
+
if (error.code !== "ENOENT")
|
|
30
|
+
throw error;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
async function privateDirectory(root, project, enforcePermissions) {
|
|
34
|
+
const ancestors = [];
|
|
35
|
+
let current = root;
|
|
36
|
+
for (;;) {
|
|
37
|
+
ancestors.unshift(current);
|
|
38
|
+
const parent = dirname(current);
|
|
39
|
+
if (parent === current)
|
|
40
|
+
break;
|
|
41
|
+
current = parent;
|
|
42
|
+
}
|
|
43
|
+
for (const path of ancestors) {
|
|
44
|
+
let info;
|
|
45
|
+
try {
|
|
46
|
+
info = await lstat(path);
|
|
47
|
+
}
|
|
48
|
+
catch (error) {
|
|
49
|
+
if (error.code !== "ENOENT")
|
|
50
|
+
throw error;
|
|
51
|
+
// Earlier ancestors have all been inspected. Never recursively follow an
|
|
52
|
+
// unchecked ancestor into the project or a different owner's directory.
|
|
53
|
+
await mkdir(path, { mode: 0o700 });
|
|
54
|
+
info = await lstat(path);
|
|
55
|
+
}
|
|
56
|
+
if (info.isSymbolicLink() || !info.isDirectory())
|
|
57
|
+
throw new Error("Setup checkpoint ancestors must be directories without symlinks");
|
|
58
|
+
}
|
|
59
|
+
const actual = await realpath(root);
|
|
60
|
+
if (actual !== root || isInside(project, actual))
|
|
61
|
+
throw new Error("Setup checkpoints must be outside the project repository");
|
|
62
|
+
if (enforcePermissions) {
|
|
63
|
+
const handle = await open(root, constants.O_RDONLY | constants.O_DIRECTORY | constants.O_NOFOLLOW);
|
|
64
|
+
try {
|
|
65
|
+
await handle.chmod(0o700);
|
|
66
|
+
const opened = await handle.stat();
|
|
67
|
+
const current = await lstat(root);
|
|
68
|
+
if (!current.isDirectory() ||
|
|
69
|
+
current.isSymbolicLink() ||
|
|
70
|
+
current.dev !== opened.dev ||
|
|
71
|
+
current.ino !== opened.ino ||
|
|
72
|
+
(opened.mode & 0o077) !== 0)
|
|
73
|
+
throw new Error("Setup checkpoint directory changed during inspection");
|
|
74
|
+
}
|
|
75
|
+
finally {
|
|
76
|
+
await handle.close();
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
12
80
|
export function defaultSetupStateDirectory(env = process.env) {
|
|
13
81
|
if (platform() === "darwin")
|
|
14
82
|
return join(homedir(), "Library", "Application Support", "Hue", "setup");
|
|
@@ -73,8 +141,8 @@ function validState(value, runId, projectRoot) {
|
|
|
73
141
|
typeof plan === "object" &&
|
|
74
142
|
!Array.isArray(plan) &&
|
|
75
143
|
hasExactKeys(plan, ["steps", "mutatesProject", "backendRequired"]) &&
|
|
76
|
-
JSON.stringify(
|
|
77
|
-
plan.mutatesProject ===
|
|
144
|
+
[JSON.stringify(STEPS), JSON.stringify(LEGACY_STEPS)].includes(JSON.stringify(plan.steps)) &&
|
|
145
|
+
plan.mutatesProject === true &&
|
|
78
146
|
plan.backendRequired === true);
|
|
79
147
|
}
|
|
80
148
|
export class FileSetupCheckpointAdapter {
|
|
@@ -94,24 +162,7 @@ export class FileSetupCheckpointAdapter {
|
|
|
94
162
|
const project = await realpath(projectRoot);
|
|
95
163
|
if (isInside(project, root))
|
|
96
164
|
throw new Error("Setup checkpoints must be outside the project repository");
|
|
97
|
-
|
|
98
|
-
try {
|
|
99
|
-
info = await lstat(root);
|
|
100
|
-
}
|
|
101
|
-
catch (error) {
|
|
102
|
-
if (error.code !== "ENOENT")
|
|
103
|
-
throw error;
|
|
104
|
-
await mkdir(root, { recursive: true, mode: 0o700 });
|
|
105
|
-
info = await lstat(root);
|
|
106
|
-
}
|
|
107
|
-
if (!info.isDirectory() || info.isSymbolicLink())
|
|
108
|
-
throw new Error("Setup checkpoint directory must be private (mode 0700, no symlink)");
|
|
109
|
-
if (this.enforcesPosixPermissions) {
|
|
110
|
-
await chmod(root, 0o700);
|
|
111
|
-
info = await lstat(root);
|
|
112
|
-
if ((info.mode & 0o077) !== 0)
|
|
113
|
-
throw new Error("Setup checkpoint directory must be private (mode 0700, no symlink)");
|
|
114
|
-
}
|
|
165
|
+
await privateDirectory(root, project, this.enforcesPosixPermissions);
|
|
115
166
|
return join(root, `${runId}.json`);
|
|
116
167
|
}
|
|
117
168
|
async load(runId, projectRoot) {
|
|
@@ -124,7 +175,7 @@ export class FileSetupCheckpointAdapter {
|
|
|
124
175
|
throw new Error("Unsafe setup checkpoint symlink");
|
|
125
176
|
}
|
|
126
177
|
const noFollow = this.runtimePlatform === "win32" ? 0 : constants.O_NOFOLLOW;
|
|
127
|
-
handle = await open(path, constants.O_RDONLY | noFollow);
|
|
178
|
+
handle = await open(path, constants.O_RDONLY | noFollow | constants.O_NONBLOCK);
|
|
128
179
|
}
|
|
129
180
|
catch (error) {
|
|
130
181
|
if (error.code === "ENOENT")
|
|
@@ -148,6 +199,9 @@ export class FileSetupCheckpointAdapter {
|
|
|
148
199
|
throw new Error("Setup checkpoint integrity check failed");
|
|
149
200
|
if (!validState(state, runId, await realpath(projectRoot)))
|
|
150
201
|
throw new Error("Setup checkpoint identity or shape does not match this project");
|
|
202
|
+
if (state.phase === "local-ready" &&
|
|
203
|
+
JSON.stringify(state.plan.steps) === JSON.stringify(LEGACY_STEPS))
|
|
204
|
+
return { ...state, plan: { ...state.plan, steps: [...STEPS] } };
|
|
151
205
|
return state;
|
|
152
206
|
}
|
|
153
207
|
finally {
|
|
@@ -155,26 +209,42 @@ export class FileSetupCheckpointAdapter {
|
|
|
155
209
|
}
|
|
156
210
|
}
|
|
157
211
|
async save(state) {
|
|
212
|
+
if (!validState(state, state.runId, await realpath(state.projectRoot)))
|
|
213
|
+
throw new Error("Invalid setup checkpoint state");
|
|
158
214
|
const path = await this.pathFor(state.runId, state.projectRoot);
|
|
159
215
|
const encoded = `${JSON.stringify({ state, digest: digest(state) })}\n`;
|
|
160
216
|
if (Buffer.byteLength(encoded) > MAX_CHECKPOINT_BYTES)
|
|
161
217
|
throw new Error("Setup checkpoint exceeds 256 KiB");
|
|
162
218
|
const temporary = join(dirname(path), `.${state.runId}.${randomUUID()}.tmp`);
|
|
163
|
-
const handle = await open(temporary,
|
|
219
|
+
const handle = await open(temporary, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | constants.O_NOFOLLOW, 0o600);
|
|
164
220
|
try {
|
|
165
|
-
|
|
166
|
-
|
|
221
|
+
try {
|
|
222
|
+
await handle.writeFile(encoded);
|
|
223
|
+
await handle.sync();
|
|
224
|
+
}
|
|
225
|
+
finally {
|
|
226
|
+
await handle.close();
|
|
227
|
+
}
|
|
228
|
+
await this.pathFor(state.runId, state.projectRoot);
|
|
229
|
+
try {
|
|
230
|
+
const existing = await lstat(path);
|
|
231
|
+
if (existing.isSymbolicLink() || !existing.isFile())
|
|
232
|
+
throw new Error("Unsafe setup checkpoint target");
|
|
233
|
+
}
|
|
234
|
+
catch (error) {
|
|
235
|
+
if (error.code !== "ENOENT")
|
|
236
|
+
throw error;
|
|
237
|
+
}
|
|
238
|
+
await rename(temporary, path);
|
|
167
239
|
}
|
|
168
240
|
finally {
|
|
169
|
-
await
|
|
241
|
+
await cleanupCheckpointTemporary(temporary);
|
|
170
242
|
}
|
|
171
|
-
await rename(temporary, path);
|
|
172
243
|
// Windows cannot open a directory as a file handle for fsync. The atomic rename and
|
|
173
244
|
// per-user state directory still provide resumability there; POSIX additionally fsyncs
|
|
174
245
|
// the containing directory so the rename survives a sudden interruption.
|
|
175
246
|
if (this.runtimePlatform !== "win32") {
|
|
176
|
-
await
|
|
177
|
-
const directory = await open(dirname(path), "r");
|
|
247
|
+
const directory = await open(dirname(path), constants.O_RDONLY | constants.O_DIRECTORY | constants.O_NOFOLLOW);
|
|
178
248
|
try {
|
|
179
249
|
await directory.sync();
|
|
180
250
|
}
|
package/dist/setup/cli.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import { realpath } from "node:fs/promises";
|
|
3
3
|
import { parseArgs } from "node:util";
|
|
4
4
|
import { FileSetupCheckpointAdapter, setupRunId } from "./checkpoint.js";
|
|
5
|
+
import { SetupBackendAdapter } from "./backend.js";
|
|
5
6
|
import { detectSetupProject } from "./detect.js";
|
|
6
7
|
import { renderHumanEvent, renderJsonlEvent, renderPlainEvent, selectSetupOutputMode, } from "./render.js";
|
|
7
8
|
import { runSetup } from "./runner.js";
|
|
@@ -28,13 +29,15 @@ async function main() {
|
|
|
28
29
|
agent: { type: "boolean", default: false },
|
|
29
30
|
format: { type: "string" },
|
|
30
31
|
project: { type: "string" },
|
|
32
|
+
origin: { type: "string" },
|
|
33
|
+
restart: { type: "boolean", default: false },
|
|
31
34
|
help: { type: "boolean", short: "h", default: false },
|
|
32
35
|
},
|
|
33
36
|
});
|
|
34
37
|
}
|
|
35
38
|
catch {
|
|
36
39
|
if (!agentRequested) {
|
|
37
|
-
process.stderr.write("Usage: hue <setup|resume|status|claim> [--agent|--format plain|jsonl] [--project PATH]\n");
|
|
40
|
+
process.stderr.write("Usage: hue <setup|resume|status|claim> [--agent|--format human|plain|jsonl] [--project PATH] [--origin URL] [--restart]\n");
|
|
38
41
|
return 2;
|
|
39
42
|
}
|
|
40
43
|
const event = {
|
|
@@ -65,7 +68,7 @@ async function main() {
|
|
|
65
68
|
process.stdout.write(`${renderJsonlEvent(event)}\n`);
|
|
66
69
|
return 2;
|
|
67
70
|
}
|
|
68
|
-
process.stdout.write("Usage: hue <setup|resume|status|claim> [--agent|--format plain|jsonl] [--project PATH]\n");
|
|
71
|
+
process.stdout.write("Usage: hue <setup|resume|status|claim> [--agent|--format human|plain|jsonl] [--project PATH] [--origin URL] [--restart]\n");
|
|
69
72
|
return 0;
|
|
70
73
|
}
|
|
71
74
|
const command = parsed.positionals[0];
|
|
@@ -75,7 +78,14 @@ async function main() {
|
|
|
75
78
|
!commands.has(command) ||
|
|
76
79
|
parsed.positionals.length !== 1 ||
|
|
77
80
|
!validFormat ||
|
|
78
|
-
(parsed.values.agent && format !== undefined && format !== "jsonl")
|
|
81
|
+
(parsed.values.agent && format !== undefined && format !== "jsonl") ||
|
|
82
|
+
(parsed.values.restart &&
|
|
83
|
+
(command !== "claim" ||
|
|
84
|
+
parsed.values.agent ||
|
|
85
|
+
format === "plain" ||
|
|
86
|
+
format === "jsonl" ||
|
|
87
|
+
!process.stdin.isTTY ||
|
|
88
|
+
!process.stdout.isTTY))) {
|
|
79
89
|
if (parsed.values.agent) {
|
|
80
90
|
const event = {
|
|
81
91
|
contractVersion: SETUP_EVENT_CONTRACT_VERSION,
|
|
@@ -90,7 +100,7 @@ async function main() {
|
|
|
90
100
|
process.stdout.write(`${renderJsonlEvent(event)}\n`);
|
|
91
101
|
}
|
|
92
102
|
else
|
|
93
|
-
process.stderr.write("Usage: hue <setup|resume|status|claim> [--agent|--format plain|jsonl] [--project PATH]\n");
|
|
103
|
+
process.stderr.write("Usage: hue <setup|resume|status|claim> [--agent|--format human|plain|jsonl] [--project PATH] [--origin URL] [--restart]\n");
|
|
94
104
|
return 2;
|
|
95
105
|
}
|
|
96
106
|
const mode = selectSetupOutputMode({
|
|
@@ -107,6 +117,10 @@ async function main() {
|
|
|
107
117
|
process.once("SIGTERM", interrupt);
|
|
108
118
|
try {
|
|
109
119
|
const root = await realpath(parsed.values.project ?? process.cwd());
|
|
120
|
+
const backend = new SetupBackendAdapter({
|
|
121
|
+
projectRoot: root,
|
|
122
|
+
...(parsed.values.origin ? { origin: parsed.values.origin } : {}),
|
|
123
|
+
});
|
|
110
124
|
await runSetup({
|
|
111
125
|
command: command,
|
|
112
126
|
mode,
|
|
@@ -114,6 +128,8 @@ async function main() {
|
|
|
114
128
|
projectRoot: root,
|
|
115
129
|
project: { detect: detectSetupProject },
|
|
116
130
|
checkpoints: new FileSetupCheckpointAdapter(),
|
|
131
|
+
backend,
|
|
132
|
+
claimRestart: parsed.values.restart,
|
|
117
133
|
signal: controller.signal,
|
|
118
134
|
emit: (event) => {
|
|
119
135
|
if (event.event === "run.completed" || event.event === "run.failed")
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { SetupProjectDetection } from "./types.js";
|
|
2
|
+
import { type FileSetupInstallationStore, type SetupInstallationRecord } from "./installation.js";
|
|
3
|
+
/** A secret-free managed integration file created or replaced by setup. */
|
|
4
|
+
export interface SetupFileChange {
|
|
5
|
+
/** Project-relative managed file path. */
|
|
6
|
+
path: string;
|
|
7
|
+
/** Safe write performed during this invocation. */
|
|
8
|
+
change: "created" | "updated";
|
|
9
|
+
}
|
|
10
|
+
/** Refuses credential/config conflicts before setup makes a provisioning request. */
|
|
11
|
+
export declare function validateSetupConfiguration(store: FileSetupInstallationStore, record: Pick<SetupInstallationRecord, "managedFiles"> | undefined, project: SetupProjectDetection): Promise<void>;
|
|
12
|
+
/** Writes only secret-free, metadata-only integration modules and never executes project code. */
|
|
13
|
+
export declare function configureSetupProject(store: FileSetupInstallationStore, record: SetupInstallationRecord, project: SetupProjectDetection): Promise<SetupFileChange[]>;
|