@stacksjs/actions 0.75.277 → 0.75.281
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/project-scripts.js +132 -266
- package/package.json +43 -43
package/dist/project-scripts.js
CHANGED
|
@@ -1,281 +1,147 @@
|
|
|
1
|
-
import{createHash}from"node:crypto";import{readFileSync}from"node:fs";import{join}from"node:path";function fail(message){throw Error(message)}function
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
public IntPtr Reserved, Desktop, Title;
|
|
31
|
-
public uint X, Y, XSize, YSize, XChars, YChars, Fill, Flags;
|
|
32
|
-
public ushort Show, ReservedSize;
|
|
33
|
-
public IntPtr ReservedBytes, Input, Output, Error;
|
|
34
|
-
}
|
|
35
|
-
[StructLayout(LayoutKind.Sequential)] struct ProcessInfo {
|
|
36
|
-
public IntPtr Process, Thread;
|
|
37
|
-
public uint ProcessId, ThreadId;
|
|
38
|
-
}
|
|
39
|
-
[DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
|
|
40
|
-
static extern IntPtr CreateJobObjectW(IntPtr attributes, string name);
|
|
41
|
-
[DllImport("kernel32.dll", SetLastError = true)]
|
|
42
|
-
static extern bool SetInformationJobObject(IntPtr job, int kind, ref ExtendedLimits limits, uint size);
|
|
43
|
-
[DllImport("kernel32.dll", SetLastError = true)]
|
|
44
|
-
static extern bool QueryInformationJobObject(IntPtr job, int kind, out Accounting info, uint size, IntPtr returned);
|
|
45
|
-
[DllImport("kernel32.dll", SetLastError = true)]
|
|
46
|
-
static extern bool AssignProcessToJobObject(IntPtr job, IntPtr process);
|
|
47
|
-
[DllImport("kernel32.dll", SetLastError = true)]
|
|
48
|
-
static extern bool TerminateJobObject(IntPtr job, uint code);
|
|
49
|
-
[DllImport("kernel32.dll", SetLastError = true)]
|
|
50
|
-
static extern bool DuplicateHandle(IntPtr sourceProcess, IntPtr source, IntPtr targetProcess,
|
|
51
|
-
out IntPtr target, uint access, bool inherit, uint options);
|
|
52
|
-
[DllImport("kernel32.dll")] static extern IntPtr GetCurrentProcess();
|
|
53
|
-
[DllImport("kernel32.dll")] static extern IntPtr GetStdHandle(int kind);
|
|
54
|
-
[DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
|
|
55
|
-
static extern IntPtr CreateFileW(string name, uint access, uint share, IntPtr security,
|
|
56
|
-
uint disposition, uint flags, IntPtr template);
|
|
57
|
-
[DllImport("kernel32.dll", SetLastError = true)]
|
|
58
|
-
static extern bool PeekNamedPipe(IntPtr pipe, IntPtr buffer, uint size, IntPtr read,
|
|
59
|
-
out uint available, IntPtr remaining);
|
|
60
|
-
[DllImport("kernel32.dll", SetLastError = true)]
|
|
61
|
-
static extern bool ReadFile(IntPtr pipe, byte[] buffer, uint count, out uint read, IntPtr overlapped);
|
|
62
|
-
[DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
|
|
63
|
-
static extern bool CreateProcessW(string application, StringBuilder command, IntPtr processSecurity,
|
|
64
|
-
IntPtr threadSecurity, bool inherit, uint flags, IntPtr environment, string directory,
|
|
65
|
-
ref Startup startup, out ProcessInfo process);
|
|
66
|
-
[DllImport("kernel32.dll", SetLastError = true)] static extern uint ResumeThread(IntPtr thread);
|
|
67
|
-
[DllImport("kernel32.dll", SetLastError = true)] static extern uint WaitForSingleObject(IntPtr handle, uint timeout);
|
|
68
|
-
[DllImport("kernel32.dll", SetLastError = true)] static extern bool GetExitCodeProcess(IntPtr process, out uint code);
|
|
69
|
-
[DllImport("kernel32.dll", SetLastError = true)] static extern bool TerminateProcess(IntPtr process, uint code);
|
|
70
|
-
[DllImport("kernel32.dll", SetLastError = true)] static extern bool CloseHandle(IntPtr handle);
|
|
71
|
-
static void Require(bool success) { if (!success) throw new Win32Exception(Marshal.GetLastWin32Error()); }
|
|
72
|
-
static IntPtr InheritableStandardHandle(int kind) {
|
|
73
|
-
IntPtr source = GetStdHandle(kind);
|
|
74
|
-
if (source == IntPtr.Zero || source == new IntPtr(-1)) return IntPtr.Zero;
|
|
75
|
-
IntPtr inherited;
|
|
76
|
-
Require(DuplicateHandle(GetCurrentProcess(), source, GetCurrentProcess(), out inherited, 0, true, 2));
|
|
77
|
-
return inherited;
|
|
1
|
+
import{createHash}from"node:crypto";import{mkdtempSync,readFileSync,rmSync,writeFileSync}from"node:fs";import{tmpdir}from"node:os";import{join}from"node:path";function fail(message){throw Error(message)}function windowsCheckHost(bun,script){return`
|
|
2
|
+
import { dlopen, FFIType as f, ptr } from 'bun:ffi'
|
|
3
|
+
setInterval(() => {}, 60_000)
|
|
4
|
+
let stopRequested = false
|
|
5
|
+
let permit
|
|
6
|
+
const permitted = new Promise(resolve => { permit = resolve })
|
|
7
|
+
process.on('message', message => {
|
|
8
|
+
if (message?.stop) { stopRequested = true; permit() }
|
|
9
|
+
else if (message?.start) permit()
|
|
10
|
+
})
|
|
11
|
+
let api
|
|
12
|
+
let lifetime = 0n
|
|
13
|
+
let job = 0n
|
|
14
|
+
let childProcess = 0n
|
|
15
|
+
let childThread = 0n
|
|
16
|
+
let failure = ''
|
|
17
|
+
let exitCode = 1
|
|
18
|
+
const wide = value => Uint16Array.from({ length: value.length + 1 }, (_, i) => i === value.length ? 0 : value.charCodeAt(i))
|
|
19
|
+
const must = (condition, operation) => {
|
|
20
|
+
if (!condition) throw new Error('Native Windows ' + operation + ' failed (Win32 ' + api.GetLastError() + ')')
|
|
21
|
+
}
|
|
22
|
+
const makeJob = () => {
|
|
23
|
+
const handle = api.CreateJobObjectW(null, null)
|
|
24
|
+
must(handle !== 0n, 'job creation')
|
|
25
|
+
const limits = new BigUint64Array(18) // JOBOBJECT_EXTENDED_LIMIT_INFORMATION, Win64.
|
|
26
|
+
new DataView(limits.buffer).setUint32(16, 0x2000, true) // KILL_ON_JOB_CLOSE.
|
|
27
|
+
if (!api.SetInformationJobObject(handle, 9, ptr(limits), limits.byteLength)) {
|
|
28
|
+
api.CloseHandle(handle)
|
|
29
|
+
must(false, 'job limits')
|
|
78
30
|
}
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
31
|
+
return handle
|
|
32
|
+
}
|
|
33
|
+
const inherited = source => {
|
|
34
|
+
const result = new BigUint64Array(1)
|
|
35
|
+
const current = api.GetCurrentProcess()
|
|
36
|
+
must(api.DuplicateHandle(current, source, current, ptr(result), 0, 1, 2), 'standard handle inheritance')
|
|
37
|
+
return result[0]
|
|
38
|
+
}
|
|
39
|
+
try {
|
|
40
|
+
api = dlopen('kernel32.dll', {
|
|
41
|
+
GetLastError: { args: [], returns: f.u32 },
|
|
42
|
+
GetCurrentProcess: { args: [], returns: f.u64 },
|
|
43
|
+
GetStdHandle: { args: [f.u32], returns: f.u64 },
|
|
44
|
+
CreateJobObjectW: { args: [f.ptr, f.ptr], returns: f.u64 },
|
|
45
|
+
SetInformationJobObject: { args: [f.u64, f.i32, f.ptr, f.u32], returns: f.i32 },
|
|
46
|
+
QueryInformationJobObject: { args: [f.u64, f.i32, f.ptr, f.u32, f.ptr], returns: f.i32 },
|
|
47
|
+
AssignProcessToJobObject: { args: [f.u64, f.u64], returns: f.i32 },
|
|
48
|
+
TerminateJobObject: { args: [f.u64, f.u32], returns: f.i32 },
|
|
49
|
+
DuplicateHandle: { args: [f.u64, f.u64, f.u64, f.ptr, f.u32, f.i32, f.u32], returns: f.i32 },
|
|
50
|
+
CreateFileW: { args: [f.ptr, f.u32, f.u32, f.ptr, f.u32, f.u32, f.u64], returns: f.u64 },
|
|
51
|
+
CreateProcessW: { args: [f.ptr, f.ptr, f.ptr, f.ptr, f.i32, f.u32, f.ptr, f.ptr, f.ptr, f.ptr], returns: f.i32 },
|
|
52
|
+
ResumeThread: { args: [f.u64], returns: f.u32 },
|
|
53
|
+
WaitForSingleObject: { args: [f.u64, f.u32], returns: f.u32 },
|
|
54
|
+
GetExitCodeProcess: { args: [f.u64, f.ptr], returns: f.i32 },
|
|
55
|
+
TerminateProcess: { args: [f.u64, f.u32], returns: f.i32 },
|
|
56
|
+
CloseHandle: { args: [f.u64], returns: f.i32 },
|
|
57
|
+
}).symbols
|
|
58
|
+
// Own the host before it can create a consumer. Parent death closes this
|
|
59
|
+
// non-inheritable lifetime handle and terminates all remaining descendants.
|
|
60
|
+
lifetime = makeJob()
|
|
61
|
+
must(api.AssignProcessToJobObject(lifetime, api.GetCurrentProcess()), 'host ownership')
|
|
62
|
+
job = makeJob()
|
|
63
|
+
process.send({ ready: true })
|
|
64
|
+
await permitted
|
|
65
|
+
if (!stopRequested) {
|
|
66
|
+
const startup = new BigUint64Array(13) // STARTUPINFOW, Win64: 104 bytes.
|
|
67
|
+
const fields = new DataView(startup.buffer)
|
|
68
|
+
fields.setUint32(0, startup.byteLength, true)
|
|
69
|
+
fields.setUint32(60, 0x100, true) // STARTF_USESTDHANDLES.
|
|
70
|
+
const inputName = wide('NUL')
|
|
71
|
+
const sourceInput = api.CreateFileW(ptr(inputName), 0x80000000, 3, null, 3, 0, 0n)
|
|
72
|
+
must(sourceInput !== 0xffffffffffffffffn, 'consumer stdin')
|
|
73
|
+
const handles = []
|
|
82
74
|
try {
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
byte[] command = new byte[1];
|
|
110
|
-
uint read;
|
|
111
|
-
Require(ReadFile(GetStdHandle(-10), command, 1, out read, IntPtr.Zero));
|
|
112
|
-
return read == 1 && command[0] == 1;
|
|
75
|
+
handles.push(inherited(sourceInput))
|
|
76
|
+
handles.push(inherited(api.GetStdHandle(0xfffffff5))) // STD_OUTPUT_HANDLE.
|
|
77
|
+
handles.push(inherited(api.GetStdHandle(0xfffffff4))) // STD_ERROR_HANDLE.
|
|
78
|
+
for (let i = 0; i < handles.length; i++) fields.setBigUint64(80 + i * 8, handles[i], true)
|
|
79
|
+
const processInfo = new BigUint64Array(3) // PROCESS_INFORMATION, Win64.
|
|
80
|
+
const executable = wide(${JSON.stringify(bun)})
|
|
81
|
+
const commandLine = wide(${JSON.stringify(`"${bun}" run ${script}`)})
|
|
82
|
+
const created = api.CreateProcessW(ptr(executable), ptr(commandLine), null, null, 1, 0x08000004, null, null, ptr(startup), ptr(processInfo))
|
|
83
|
+
childProcess = processInfo[0]
|
|
84
|
+
childThread = processInfo[1]
|
|
85
|
+
must(created, 'suspended consumer creation')
|
|
86
|
+
}
|
|
87
|
+
finally {
|
|
88
|
+
api.CloseHandle(sourceInput)
|
|
89
|
+
for (const handle of handles) api.CloseHandle(handle)
|
|
90
|
+
}
|
|
91
|
+
// No user instruction runs until both native ownership jobs contain it.
|
|
92
|
+
must(api.AssignProcessToJobObject(job, childProcess), 'consumer ownership')
|
|
93
|
+
must(api.ResumeThread(childThread) !== 0xffffffff, 'consumer startup')
|
|
94
|
+
while (!stopRequested) {
|
|
95
|
+
const state = api.WaitForSingleObject(childProcess, 0)
|
|
96
|
+
if (state === 0) {
|
|
97
|
+
const code = new Uint32Array(1)
|
|
98
|
+
must(api.GetExitCodeProcess(childProcess, ptr(code)), 'consumer exit status')
|
|
99
|
+
exitCode = code[0] | 0
|
|
100
|
+
break
|
|
113
101
|
}
|
|
114
|
-
|
|
102
|
+
must(state === 258, 'consumer wait') // WAIT_TIMEOUT.
|
|
103
|
+
await Bun.sleep(10)
|
|
115
104
|
}
|
|
116
105
|
}
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
limits.Basic.Flags = 0x2000; // JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE
|
|
122
|
-
try { Require(SetInformationJobObject(job, 9, ref limits, (uint)Marshal.SizeOf(typeof(ExtendedLimits)))); }
|
|
123
|
-
catch { CloseHandle(job); throw; }
|
|
124
|
-
return job;
|
|
125
|
-
}
|
|
126
|
-
public static int Run(string bun, string script) {
|
|
127
|
-
// The control pipe is never inherited as consumer stdin. Prepare both
|
|
128
|
-
// ownership jobs before reporting readiness or accepting startup permission.
|
|
129
|
-
if (StopRequested()) return 1;
|
|
130
|
-
// The outer job owns this host before it creates anything. If cancellation
|
|
131
|
-
// races suspended-child assignment below, host death still kills that child.
|
|
132
|
-
// Retain this non-inheritable handle until process exit; closing it earlier
|
|
133
|
-
// would terminate this host before it can return the command's exit code.
|
|
134
|
-
IntPtr lifetime = Job();
|
|
135
|
-
Require(AssignProcessToJobObject(lifetime, GetCurrentProcess()));
|
|
136
|
-
IntPtr commandJob = Job();
|
|
137
|
-
ProcessInfo child = new ProcessInfo();
|
|
106
|
+
}
|
|
107
|
+
catch (error) { failure = String(error) }
|
|
108
|
+
try {
|
|
109
|
+
if (api && job !== 0n) {
|
|
138
110
|
try {
|
|
139
|
-
if (
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
// Bun can supply non-inheritable standard handles to PowerShell. A
|
|
144
|
-
// native CreateProcess child needs explicitly inheritable duplicates.
|
|
145
|
-
// Never execute user code before it belongs to the inner cleanup job.
|
|
146
|
-
try {
|
|
147
|
-
startup.Input = ConsumerInput();
|
|
148
|
-
startup.Output = InheritableStandardHandle(-11);
|
|
149
|
-
startup.Error = InheritableStandardHandle(-12);
|
|
150
|
-
Require(CreateProcessW(bun, new StringBuilder("\\"" + bun + "\\" run " + script),
|
|
151
|
-
IntPtr.Zero, IntPtr.Zero, true, 0x08000004, IntPtr.Zero, null, ref startup, out child));
|
|
152
|
-
} finally {
|
|
153
|
-
foreach (IntPtr handle in new[] { startup.Input, startup.Output, startup.Error })
|
|
154
|
-
if (handle != IntPtr.Zero) CloseHandle(handle);
|
|
111
|
+
if (childProcess !== 0n) {
|
|
112
|
+
const state = api.WaitForSingleObject(childProcess, 0)
|
|
113
|
+
must(state === 0 || state === 258, 'final consumer wait')
|
|
114
|
+
if (state === 258) must(api.TerminateProcess(childProcess, 1), 'suspended consumer disposal')
|
|
155
115
|
}
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
uint code = 1;
|
|
166
|
-
if (!cancelled) Require(GetExitCodeProcess(child.Process, out code));
|
|
167
|
-
return unchecked((int)code);
|
|
168
|
-
} finally {
|
|
169
|
-
try {
|
|
170
|
-
// Also dispose a suspended child if assigning its inner job failed.
|
|
171
|
-
if (child.Process != IntPtr.Zero) {
|
|
172
|
-
uint state = WaitForSingleObject(child.Process, 0);
|
|
173
|
-
Require(state == 0 || state == 258);
|
|
174
|
-
if (state == 258) Require(TerminateProcess(child.Process, 1));
|
|
175
|
-
}
|
|
176
|
-
// Membership survives an exited intermediate process, unlike a PID
|
|
177
|
-
// tree. Query it on every exit path before closing the owning handle.
|
|
178
|
-
// https://learn.microsoft.com/windows/win32/procthread/job-objects
|
|
179
|
-
Require(TerminateJobObject(commandJob, 1));
|
|
180
|
-
var cleanup = System.Diagnostics.Stopwatch.StartNew();
|
|
181
|
-
while (true) {
|
|
182
|
-
Accounting info;
|
|
183
|
-
Require(QueryInformationJobObject(commandJob, 1, out info, (uint)Marshal.SizeOf(typeof(Accounting)), IntPtr.Zero));
|
|
184
|
-
if (info.ActiveProcesses == 0 && (child.Process == IntPtr.Zero || WaitForSingleObject(child.Process, 0) == 0)) break;
|
|
185
|
-
if (cleanup.ElapsedMilliseconds >= 1000) throw new TimeoutException("Owned Windows processes did not finish cleanup");
|
|
186
|
-
Thread.Sleep(10);
|
|
187
|
-
}
|
|
188
|
-
} finally {
|
|
189
|
-
if (child.Thread != IntPtr.Zero) CloseHandle(child.Thread);
|
|
190
|
-
if (child.Process != IntPtr.Zero) CloseHandle(child.Process);
|
|
191
|
-
Require(CloseHandle(commandJob));
|
|
116
|
+
must(api.TerminateJobObject(job, 1), 'owned process termination')
|
|
117
|
+
const accounting = new BigUint64Array(6)
|
|
118
|
+
const fields = new DataView(accounting.buffer)
|
|
119
|
+
const deadline = performance.now() + 1000
|
|
120
|
+
while (true) {
|
|
121
|
+
must(api.QueryInformationJobObject(job, 1, ptr(accounting), accounting.byteLength, null), 'owned process accounting')
|
|
122
|
+
if (fields.getUint32(40, true) === 0 && (childProcess === 0n || api.WaitForSingleObject(childProcess, 0) === 0)) break
|
|
123
|
+
if (performance.now() >= deadline) throw new Error('Owned Windows processes did not finish cleanup')
|
|
124
|
+
await Bun.sleep(10)
|
|
192
125
|
}
|
|
193
126
|
}
|
|
127
|
+
finally {
|
|
128
|
+
if (childThread !== 0n) api.CloseHandle(childThread)
|
|
129
|
+
if (childProcess !== 0n) api.CloseHandle(childProcess)
|
|
130
|
+
must(api.CloseHandle(job), 'owned job handle disposal')
|
|
131
|
+
}
|
|
194
132
|
}
|
|
195
133
|
}
|
|
196
|
-
'
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
let
|
|
203
|
-
let start
|
|
204
|
-
const permitted = new Promise(resolve => { start = resolve })
|
|
205
|
-
process.on('message', async (message) => {
|
|
206
|
-
if (message?.stop) {
|
|
207
|
-
stopRequested = true
|
|
208
|
-
start()
|
|
209
|
-
if (command) {
|
|
210
|
-
try {
|
|
211
|
-
if (!consumerPermitted) {
|
|
212
|
-
// Preparation may itself spawn the C# compiler. Before permission,
|
|
213
|
-
// the entire helper tree is disposable without interrupting user code.
|
|
214
|
-
const killed = Bun.spawnSync([${JSON.stringify(windowsTaskkill)}, '/PID', String(command.pid), '/T', '/F'], { stdout: 'ignore', stderr: 'ignore', timeout: 2000 })
|
|
215
|
-
if (killed.exitCode !== 0 && command.exitCode === null) throw new Error('Native preparation cleanup failed')
|
|
216
|
-
}
|
|
217
|
-
else {
|
|
218
|
-
command.stdin.write(new Uint8Array([2]))
|
|
219
|
-
await command.stdin.flush()
|
|
220
|
-
}
|
|
221
|
-
} catch (error) { process.send({ error: String(error) }) }
|
|
222
|
-
}
|
|
223
|
-
} else if (message?.start) start()
|
|
224
|
-
})
|
|
225
|
-
process.send({ ready: true })
|
|
226
|
-
await permitted
|
|
227
|
-
if (stopRequested) process.send({ exitCode: 1 })
|
|
228
|
-
else {
|
|
134
|
+
catch (error) { failure += (failure ? '; ' : '') + String(error) }
|
|
135
|
+
// Keep the lifetime job open until the parent disposes this owned Bun process.
|
|
136
|
+
// Closing it here would kill this host before its completion report is sent.
|
|
137
|
+
if (failure) process.send({ error: failure })
|
|
138
|
+
else process.send({ exitCode })
|
|
139
|
+
`}export async function runProjectScripts(options){const timeoutMs=options.timeoutMs??300000;if(!Number.isSafeInteger(timeoutMs)||timeoutMs<=0||!options.scripts.length||options.scripts.some((name)=>!/^[a-z0-9][\w:.-]*$/i.test(name))||new Set(options.scripts).size!==options.scripts.length)fail("Project checks need unique script names and a positive execution budget");const receipt={bunVersion:"",checks:[]},manifestPath=join(options.cwd,"package.json"),originalManifest=readFileSync(manifestPath,"utf8"),manifest=JSON.parse(originalManifest),bun=Bun.which("bun",{cwd:options.cwd});if(!bun)fail("Consumer Bun executable is unavailable");const probe=Bun.spawnSync([bun,"--version"],{cwd:options.cwd,stdout:"pipe",stderr:"pipe",timeout:1e4}),bunVersion=probe.stdout.toString().trim();if(probe.exitCode!==0||!/^\d+\.\d+\.\d+(?:-[\w.-]+)?$/.test(bunVersion)||!Bun.semver.satisfies(bunVersion,manifest.engines?.bun??">=1.3.0"))fail("Consumer Bun executable does not satisfy the declared toolchain");receipt.bunVersion=bunVersion;const hostDirectory=mkdtempSync(join(tmpdir(),"stacks-project-host-"));try{const hostConfig=join(hostDirectory,"bunfig.toml");writeFileSync(hostConfig,`preload = []
|
|
140
|
+
`);for(const script of options.scripts){if(!Object.hasOwn(manifest.scripts??{},script)||typeof manifest.scripts?.[script]!=="string"||!manifest.scripts[script].trim())fail(`Consumer script ${script} is not declared`);options.signal?.throwIfAborted();const windows=process.platform==="win32",command=[bun,"run",script];let reportExit=()=>{},reportFailure=()=>{};const reported=new Promise((resolve,reject)=>{reportExit=resolve;reportFailure=reject});let interrupted=!1;const host=windows?windowsCheckHost(bun,script):`setInterval(() => {}, 60_000)
|
|
229
141
|
try {
|
|
230
|
-
command = Bun.spawn(${JSON.stringify(command)}, { stdin: '
|
|
231
|
-
process.send({
|
|
232
|
-
const forward = async () => {
|
|
233
|
-
const reader = command.stdout.getReader()
|
|
234
|
-
let prefix = Buffer.alloc(0)
|
|
235
|
-
let ready = false
|
|
236
|
-
try {
|
|
237
|
-
while (true) {
|
|
238
|
-
const next = await reader.read()
|
|
239
|
-
if (next.done) break
|
|
240
|
-
if (ready) { await Bun.write(Bun.stdout, next.value); continue }
|
|
241
|
-
prefix = Buffer.concat([prefix, Buffer.from(next.value)])
|
|
242
|
-
const line = prefix.indexOf(10)
|
|
243
|
-
if (line < 0) {
|
|
244
|
-
if (prefix.length > 128) throw new Error('Native ownership readiness was malformed')
|
|
245
|
-
continue
|
|
246
|
-
}
|
|
247
|
-
if (prefix.subarray(0, line).toString().trimEnd() !== 'STACKS_OWNED_CHECK_READY')
|
|
248
|
-
throw new Error('Native ownership readiness was malformed')
|
|
249
|
-
ready = true
|
|
250
|
-
if (!stopRequested) {
|
|
251
|
-
// This assignment and write share one event-loop turn. A later
|
|
252
|
-
// stop therefore uses cooperative Job Object cleanup, never the
|
|
253
|
-
// preparation-only kill once user code could be running.
|
|
254
|
-
consumerPermitted = true
|
|
255
|
-
command.stdin.write(new Uint8Array([1]))
|
|
256
|
-
await command.stdin.flush()
|
|
257
|
-
}
|
|
258
|
-
if (prefix.length > line + 1) await Bun.write(Bun.stdout, prefix.subarray(line + 1))
|
|
259
|
-
prefix = Buffer.alloc(0)
|
|
260
|
-
}
|
|
261
|
-
if (!ready && !stopRequested) throw new Error('Native ownership readiness was not received')
|
|
262
|
-
} finally { reader.releaseLock() }
|
|
263
|
-
}
|
|
264
|
-
const [code] = await Promise.all([command.exited, forward()])
|
|
265
|
-
process.send({ exitCode: code })
|
|
142
|
+
const command = Bun.spawn(${JSON.stringify(command)}, { stdin: 'ignore', stdout: 'inherit', stderr: 'inherit' })
|
|
143
|
+
process.send({ exitCode: await command.exited })
|
|
266
144
|
} catch (error) {
|
|
267
|
-
if (command && !consumerPermitted) {
|
|
268
|
-
try { command.kill('SIGKILL'); await command.exited } catch {}
|
|
269
|
-
}
|
|
270
145
|
process.send({ error: String(error) })
|
|
271
146
|
}
|
|
272
|
-
}
|
|
273
|
-
`:`
|
|
274
|
-
setInterval(() => {}, 60_000)
|
|
275
|
-
try {
|
|
276
|
-
const command = Bun.spawn(${JSON.stringify(command)}, { stdin: 'ignore', stdout: 'inherit', stderr: 'inherit' })
|
|
277
|
-
process.send({ exitCode: await command.exited })
|
|
278
|
-
} catch (error) {
|
|
279
|
-
process.send({ error: String(error) })
|
|
280
|
-
}
|
|
281
|
-
`,child=Bun.spawn([bun,"--eval",host],{cwd:options.cwd,env:{...process.env,...options.env},stdin:"ignore",stdout:"pipe",stderr:"pipe",detached:process.platform!=="win32",ipc(message){if(powershell&&message?.ready===!0)try{child.send(interrupted?{stop:!0}:{start:!0})}catch(error){reportFailure(Error(`Consumer script ${script} control channel failed: ${String(error)}`))}else if(powershell&&Number.isSafeInteger(message?.helperPid)&&message.helperPid>0)windowsHelperPid=message.helperPid;else if(Number.isInteger(message?.exitCode))reportExit(message.exitCode);else reportFailure(Error(`Consumer script ${script} host failed: ${message?.error??"invalid completion report"}`))}}),pipes=[child.stdout,child.stderr].map((stream,index)=>({reader:stream.getReader(),done:!1,source:index===0?"stdout":"stderr"}));let teardown,announceStop=()=>{};const stopped=new Promise((resolve)=>{announceStop=resolve}),waitWithin=async(promise,milliseconds)=>{let timer;try{return await Promise.race([promise,new Promise((_,reject)=>{timer=setTimeout(()=>reject(Error("Process or output cleanup did not finish within its grace period")),milliseconds)})])}finally{if(timer)clearTimeout(timer)}};let draining;const dispose=(preserveOutput)=>{if(teardown)return teardown;teardown=(async()=>{const errors=[];if(windowsTaskkill){if(!preserveOutput)try{child.send({stop:!0});await waitWithin(reported,1000)}catch{errors.push("Native Windows cleanup completion was not confirmed")}try{const result=Bun.spawnSync([windowsTaskkill,"/PID",String(child.pid),"/T","/F"],{stdout:"ignore",stderr:"ignore",timeout:2000});if(result.exitCode!==0)errors.push(`taskkill failed with exit code ${result.exitCode}`)}catch{errors.push("Windows process-tree cleanup could not execute taskkill")}if(windowsHelperPid){const until=performance.now()+1000;let alive=!0;while(alive){try{process.kill(windowsHelperPid,0)}catch(error){if(error.code==="ESRCH")alive=!1;else break}if(!alive||performance.now()>=until)break;await new Promise((resolve)=>setTimeout(resolve,10))}if(alive)errors.push("Native Windows helper disposal was not confirmed")}if(errors.length)try{child.kill("SIGKILL")}catch{errors.push("Direct child cleanup also failed")}}else{const signalGroup=(signal)=>{try{process.kill(-child.pid,signal)}catch(error){if(error.code!=="ESRCH"){const cause=error;errors.push(`Process-group ${signal} failed (code=${cause.code}, errno=${cause.errno}, message=${cause.message})`)}}};signalGroup("SIGTERM");await new Promise((resolve)=>setTimeout(resolve,500));signalGroup("SIGKILL");const until=performance.now()+1000;let pendingError="";while(!0){try{process.kill(-child.pid,0)}catch(error){const cause=error;if(cause.code==="ESRCH")break;pendingError=`signal=0, code=${cause.code}, errno=${cause.errno}, message=${cause.message}`;if(cause.code!=="EPERM"){errors.push(`Process-group disposal could not be confirmed (${pendingError})`);break}}if(performance.now()>=until){errors.push(`Owned process group remained after cleanup${pendingError?` (${pendingError})`:""}`);break}await new Promise((resolve)=>setTimeout(resolve,10))}}if(preserveOutput)try{await waitWithin(draining,1000)}catch{errors.push("Output draining exceeded its cleanup grace period")}const close=Promise.all([...pipes.filter((pipe)=>!pipe.done).map(async(pipe)=>{try{await pipe.reader.cancel()}catch{errors.push("Output reader cancellation failed")}}),child.exited]);try{await waitWithin(close,1000)}catch{errors.push("Process or output cleanup exceeded its grace period")}if(errors.length)fail(`Consumer script ${script} cleanup failed: ${errors.join("; ")}`)})();teardown.catch(()=>{});return teardown},stop=()=>{if(interrupted)return;interrupted=!0;dispose(!1);announceStop()};child.exited.then((code)=>{if(!teardown)reportFailure(Error(`Consumer script ${script} host exited before reporting completion (exit code ${code})`))});const deadline=setTimeout(stop,timeoutMs);options.signal?.addEventListener("abort",stop,{once:!0});if(options.signal?.aborted)stop();const output=createHash("sha256");async function drain(pipe){try{while(!0){const next=await pipe.reader.read();if(next.done)break;output.update(next.value);options.onOutput?.(next.value.slice(),pipe.source)}}finally{pipe.done=!0;pipe.reader.releaseLock()}}let exitCode;try{draining=Promise.all(pipes.map(drain)).then(()=>{});draining.catch(reportFailure);const completion=reported.then((exitCode)=>({kind:"completed",exitCode})),result=await Promise.race([completion,stopped.then(()=>({kind:"interrupted"}))]);if(result.kind==="interrupted"||interrupted){await teardown;fail(`Consumer script ${script} was interrupted or exceeded its execution budget`)}exitCode=result.exitCode;await dispose(!0)}catch(error){const wasInterrupted=interrupted;stop();try{await teardown}catch(cleanupError){if(wasInterrupted)fail(`Consumer script ${script} was interrupted or exceeded its execution budget; ${cleanupError instanceof Error?cleanupError.message:String(cleanupError)}`);throw cleanupError}throw error}finally{clearTimeout(deadline);options.signal?.removeEventListener("abort",stop)}if(interrupted||options.signal?.aborted)fail(`Consumer script ${script} was interrupted or exceeded its execution budget`);if(exitCode!==0)fail(`Consumer script ${script} failed with exit code ${exitCode}`);if(readFileSync(manifestPath,"utf8")!==originalManifest)fail(`Consumer script ${script} changed the project manifest`);receipt.checks.push({script,exitCode:0,outputSha256:output.digest("hex")})}return receipt}
|
|
147
|
+
`,child=Bun.spawn([bun,`--config=${hostConfig}`,"--eval",host],{cwd:options.cwd,env:{...process.env,...options.env},stdin:"ignore",stdout:"pipe",stderr:"pipe",detached:process.platform!=="win32",ipc(message){if(windows&&message?.ready===!0)try{child.send(interrupted?{stop:!0}:{start:!0})}catch(error){reportFailure(Error(`Consumer script ${script} control channel failed: ${String(error)}`))}else if(Number.isInteger(message?.exitCode))reportExit(message.exitCode);else reportFailure(Error(`Consumer script ${script} host failed: ${message?.error??"invalid completion report"}`))}}),pipes=[child.stdout,child.stderr].map((stream,index)=>({reader:stream.getReader(),done:!1,source:index===0?"stdout":"stderr"}));let teardown,announceStop=()=>{};const stopped=new Promise((resolve)=>{announceStop=resolve}),waitWithin=async(promise,milliseconds)=>{let timer;try{return await Promise.race([promise,new Promise((_,reject)=>{timer=setTimeout(()=>reject(Error("Process or output cleanup did not finish within its grace period")),milliseconds)})])}finally{if(timer)clearTimeout(timer)}};let draining;const dispose=(preserveOutput)=>{if(teardown)return teardown;teardown=(async()=>{const errors=[];if(windows){if(!preserveOutput)try{child.send({stop:!0});await waitWithin(reported,1000)}catch(error){errors.push(`Native Windows cleanup completion was not confirmed: ${error instanceof Error?error.message:String(error)}`)}try{child.kill("SIGKILL")}catch{errors.push("Native Windows owner disposal failed")}}else{const signalGroup=(signal)=>{try{process.kill(-child.pid,signal)}catch(error){if(error.code!=="ESRCH"){const cause=error;errors.push(`Process-group ${signal} failed (code=${cause.code}, errno=${cause.errno}, message=${cause.message})`)}}};signalGroup("SIGTERM");await new Promise((resolve)=>setTimeout(resolve,500));signalGroup("SIGKILL");const until=performance.now()+1000;let pendingError="";while(!0){try{process.kill(-child.pid,0)}catch(error){const cause=error;if(cause.code==="ESRCH")break;pendingError=`signal=0, code=${cause.code}, errno=${cause.errno}, message=${cause.message}`;if(cause.code!=="EPERM"){errors.push(`Process-group disposal could not be confirmed (${pendingError})`);break}}if(performance.now()>=until){errors.push(`Owned process group remained after cleanup${pendingError?` (${pendingError})`:""}`);break}await new Promise((resolve)=>setTimeout(resolve,10))}}if(preserveOutput)try{await waitWithin(draining,1000)}catch{errors.push("Output draining exceeded its cleanup grace period")}const close=Promise.all([...pipes.filter((pipe)=>!pipe.done).map(async(pipe)=>{try{await pipe.reader.cancel()}catch{errors.push("Output reader cancellation failed")}}),child.exited]);try{await waitWithin(close,1000)}catch{errors.push("Process or output cleanup exceeded its grace period")}if(errors.length)fail(`Consumer script ${script} cleanup failed: ${errors.join("; ")}`)})();teardown.catch(()=>{});return teardown},stop=()=>{if(interrupted)return;interrupted=!0;dispose(!1);announceStop()};child.exited.then((code)=>{if(!teardown)reportFailure(Error(`Consumer script ${script} host exited before reporting completion (exit code ${code})`))});const deadline=setTimeout(stop,timeoutMs);options.signal?.addEventListener("abort",stop,{once:!0});if(options.signal?.aborted)stop();const output=createHash("sha256");async function drain(pipe){try{while(!0){const next=await pipe.reader.read();if(next.done)break;output.update(next.value);options.onOutput?.(next.value.slice(),pipe.source)}}finally{pipe.done=!0;pipe.reader.releaseLock()}}let exitCode;try{draining=Promise.all(pipes.map(drain)).then(()=>{});draining.catch(reportFailure);const completion=reported.then((exitCode)=>({kind:"completed",exitCode})),result=await Promise.race([completion,stopped.then(()=>({kind:"interrupted"}))]);if(result.kind==="interrupted"||interrupted){await teardown;fail(`Consumer script ${script} was interrupted or exceeded its execution budget`)}exitCode=result.exitCode;await dispose(!0)}catch(error){const wasInterrupted=interrupted;stop();try{await teardown}catch(cleanupError){if(wasInterrupted)fail(`Consumer script ${script} was interrupted or exceeded its execution budget; ${cleanupError instanceof Error?cleanupError.message:String(cleanupError)}`);throw cleanupError}throw error}finally{clearTimeout(deadline);options.signal?.removeEventListener("abort",stop)}if(interrupted||options.signal?.aborted)fail(`Consumer script ${script} was interrupted or exceeded its execution budget`);if(exitCode!==0)fail(`Consumer script ${script} failed with exit code ${exitCode}`);if(readFileSync(manifestPath,"utf8")!==originalManifest)fail(`Consumer script ${script} changed the project manifest`);receipt.checks.push({script,exitCode:0,outputSha256:output.digest("hex")})}return receipt}finally{rmSync(hostDirectory,{recursive:!0,force:!0})}}
|
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "@stacksjs/actions",
|
|
3
3
|
"type": "module",
|
|
4
4
|
"sideEffects": false,
|
|
5
|
-
"version": "0.75.
|
|
5
|
+
"version": "0.75.281",
|
|
6
6
|
"description": "The Stacks actions.",
|
|
7
7
|
"author": "Chris Breuer",
|
|
8
8
|
"contributors": [
|
|
@@ -66,55 +66,55 @@
|
|
|
66
66
|
"prepublishOnly": "bun run build"
|
|
67
67
|
},
|
|
68
68
|
"dependencies": {
|
|
69
|
-
"@stacksjs/action-runner": "0.75.
|
|
70
|
-
"@stacksjs/ai": "0.75.
|
|
71
|
-
"@stacksjs/analytics": "0.75.
|
|
72
|
-
"@stacksjs/api": "0.75.
|
|
73
|
-
"@stacksjs/auth": "0.75.
|
|
74
|
-
"@stacksjs/build": "0.75.
|
|
69
|
+
"@stacksjs/action-runner": "0.75.281",
|
|
70
|
+
"@stacksjs/ai": "0.75.281",
|
|
71
|
+
"@stacksjs/analytics": "0.75.281",
|
|
72
|
+
"@stacksjs/api": "0.75.281",
|
|
73
|
+
"@stacksjs/auth": "0.75.281",
|
|
74
|
+
"@stacksjs/build": "0.75.281",
|
|
75
75
|
"@stacksjs/bumpx": "^0.2.26",
|
|
76
76
|
"@stacksjs/bunpress": "^0.2.15",
|
|
77
|
-
"@stacksjs/cli": "0.75.
|
|
78
|
-
"@stacksjs/cloud": "0.75.
|
|
79
|
-
"@stacksjs/cms": "0.75.
|
|
80
|
-
"@stacksjs/components": "0.2.
|
|
81
|
-
"@stacksjs/config": "0.75.
|
|
82
|
-
"@stacksjs/database": "0.75.
|
|
83
|
-
"@stacksjs/desktop-build": "0.75.
|
|
84
|
-
"@stacksjs/dns": "0.75.
|
|
85
|
-
"@stacksjs/enums": "0.75.
|
|
86
|
-
"@stacksjs/env": "0.75.
|
|
87
|
-
"@stacksjs/error-handling": "0.75.
|
|
88
|
-
"@stacksjs/github": "0.75.
|
|
77
|
+
"@stacksjs/cli": "0.75.281",
|
|
78
|
+
"@stacksjs/cloud": "0.75.281",
|
|
79
|
+
"@stacksjs/cms": "0.75.281",
|
|
80
|
+
"@stacksjs/components": "0.2.676",
|
|
81
|
+
"@stacksjs/config": "0.75.281",
|
|
82
|
+
"@stacksjs/database": "0.75.281",
|
|
83
|
+
"@stacksjs/desktop-build": "0.75.281",
|
|
84
|
+
"@stacksjs/dns": "0.75.281",
|
|
85
|
+
"@stacksjs/enums": "0.75.281",
|
|
86
|
+
"@stacksjs/env": "0.75.281",
|
|
87
|
+
"@stacksjs/error-handling": "0.75.281",
|
|
88
|
+
"@stacksjs/github": "0.75.281",
|
|
89
89
|
"@stacksjs/gitit": "^0.2.5",
|
|
90
|
-
"@stacksjs/i18n": "0.75.
|
|
91
|
-
"@stacksjs/image": "0.75.
|
|
92
|
-
"@stacksjs/logging": "0.75.
|
|
90
|
+
"@stacksjs/i18n": "0.75.281",
|
|
91
|
+
"@stacksjs/image": "0.75.281",
|
|
92
|
+
"@stacksjs/logging": "0.75.281",
|
|
93
93
|
"@stacksjs/logsmith": "^0.2.11",
|
|
94
|
-
"@stacksjs/mobile": "0.75.
|
|
95
|
-
"@stacksjs/orm": "0.75.
|
|
96
|
-
"@stacksjs/path": "0.75.
|
|
97
|
-
"@stacksjs/payments": "0.75.
|
|
98
|
-
"@stacksjs/queue": "0.75.
|
|
99
|
-
"@stacksjs/registry": "0.75.
|
|
100
|
-
"@stacksjs/router": "0.75.
|
|
94
|
+
"@stacksjs/mobile": "0.75.281",
|
|
95
|
+
"@stacksjs/orm": "0.75.281",
|
|
96
|
+
"@stacksjs/path": "0.75.281",
|
|
97
|
+
"@stacksjs/payments": "0.75.281",
|
|
98
|
+
"@stacksjs/queue": "0.75.281",
|
|
99
|
+
"@stacksjs/registry": "0.75.281",
|
|
100
|
+
"@stacksjs/router": "0.75.281",
|
|
101
101
|
"@stacksjs/rpx": "^0.11.54",
|
|
102
|
-
"@stacksjs/scheduler": "0.75.
|
|
103
|
-
"@stacksjs/search-engine": "0.75.
|
|
104
|
-
"@stacksjs/security": "0.75.
|
|
105
|
-
"@stacksjs/server": "0.75.
|
|
106
|
-
"@stacksjs/sites": "0.75.
|
|
107
|
-
"@stacksjs/storage": "0.75.
|
|
108
|
-
"@stacksjs/strings": "0.75.
|
|
109
|
-
"@stacksjs/stx": "0.2.
|
|
102
|
+
"@stacksjs/scheduler": "0.75.281",
|
|
103
|
+
"@stacksjs/search-engine": "0.75.281",
|
|
104
|
+
"@stacksjs/security": "0.75.281",
|
|
105
|
+
"@stacksjs/server": "0.75.281",
|
|
106
|
+
"@stacksjs/sites": "0.75.281",
|
|
107
|
+
"@stacksjs/storage": "0.75.281",
|
|
108
|
+
"@stacksjs/strings": "0.75.281",
|
|
109
|
+
"@stacksjs/stx": "0.2.676",
|
|
110
110
|
"@stacksjs/tlsx": "^0.13.19",
|
|
111
111
|
"@stacksjs/ts-cloud": "^0.16.58",
|
|
112
112
|
"@stacksjs/ts-md": "^0.1.1",
|
|
113
|
-
"@stacksjs/tunnel": "0.75.
|
|
114
|
-
"@stacksjs/types": "0.75.
|
|
115
|
-
"@stacksjs/utils": "0.75.
|
|
116
|
-
"@stacksjs/whois": "0.75.
|
|
117
|
-
"bun-plugin-stx": "0.2.
|
|
113
|
+
"@stacksjs/tunnel": "0.75.281",
|
|
114
|
+
"@stacksjs/types": "0.75.281",
|
|
115
|
+
"@stacksjs/utils": "0.75.281",
|
|
116
|
+
"@stacksjs/whois": "0.75.281",
|
|
117
|
+
"bun-plugin-stx": "0.2.676"
|
|
118
118
|
},
|
|
119
119
|
"peerDependencies": {
|
|
120
120
|
"craft-native": ">=0.0.301",
|
|
@@ -129,7 +129,7 @@
|
|
|
129
129
|
}
|
|
130
130
|
},
|
|
131
131
|
"devDependencies": {
|
|
132
|
-
"@stacksjs/validation": "0.75.
|
|
132
|
+
"@stacksjs/validation": "0.75.281",
|
|
133
133
|
"better-dx": "^0.2.24"
|
|
134
134
|
}
|
|
135
135
|
}
|