@xuda.io/ai_module 1.1.5658 → 1.1.5660
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/index.mjs +291 -118
- package/index_ms.mjs +10 -10
- package/index_msa.mjs +10 -10
- package/package.json +1 -1
- package/{xudex_engines.mjs → xucode_engines.mjs} +74 -7
- package/{xudex_mirror.mjs → xucode_mirror.mjs} +115 -8
- package/{xudex_preview.mjs → xucode_preview.mjs} +5 -5
- package/xucode_proxy.mjs +426 -0
- package/{xudex_run.mjs → xucode_run.mjs} +111 -14
- package/{xudex_runtime.mjs → xucode_runtime.mjs} +13 -7
- package/{xudex_tracker.mjs → xucode_tracker.mjs} +40 -10
- package/{xudex_verify.mjs → xucode_verify.mjs} +5 -5
- package/{xudex_vm.mjs → xucode_vm.mjs} +69 -21
package/index_ms.mjs
CHANGED
|
@@ -69,28 +69,28 @@ export const git_repo_list = async function (...args) {
|
|
|
69
69
|
return await broker.send_to_queue("git_repo_list", ...args);
|
|
70
70
|
};
|
|
71
71
|
|
|
72
|
-
export const
|
|
73
|
-
return await broker.send_to_queue("
|
|
72
|
+
export const xucode_key_set = async function (...args) {
|
|
73
|
+
return await broker.send_to_queue("xucode_key_set", ...args);
|
|
74
74
|
};
|
|
75
75
|
|
|
76
|
-
export const
|
|
77
|
-
return await broker.send_to_queue("
|
|
76
|
+
export const xucode_key_list = async function (...args) {
|
|
77
|
+
return await broker.send_to_queue("xucode_key_list", ...args);
|
|
78
78
|
};
|
|
79
79
|
|
|
80
|
-
export const
|
|
81
|
-
return await broker.send_to_queue("
|
|
80
|
+
export const xucode_key_delete = async function (...args) {
|
|
81
|
+
return await broker.send_to_queue("xucode_key_delete", ...args);
|
|
82
82
|
};
|
|
83
83
|
|
|
84
|
-
export const
|
|
85
|
-
return await broker.send_to_queue("
|
|
84
|
+
export const xucode_engine_list = async function (...args) {
|
|
85
|
+
return await broker.send_to_queue("xucode_engine_list", ...args);
|
|
86
86
|
};
|
|
87
87
|
|
|
88
88
|
export const git_repo_disconnect = async function (...args) {
|
|
89
89
|
return await broker.send_to_queue("git_repo_disconnect", ...args);
|
|
90
90
|
};
|
|
91
91
|
|
|
92
|
-
export const
|
|
93
|
-
return await broker.send_to_queue("
|
|
92
|
+
export const xucode_run = async function (...args) {
|
|
93
|
+
return await broker.send_to_queue("xucode_run", ...args);
|
|
94
94
|
};
|
|
95
95
|
|
|
96
96
|
export const git_repo_status = async function (...args) {
|
package/index_msa.mjs
CHANGED
|
@@ -69,28 +69,28 @@ export const git_repo_list = function (...args) {
|
|
|
69
69
|
broker.send_to_queue_async("git_repo_list", ...args);
|
|
70
70
|
};
|
|
71
71
|
|
|
72
|
-
export const
|
|
73
|
-
broker.send_to_queue_async("
|
|
72
|
+
export const xucode_key_set = function (...args) {
|
|
73
|
+
broker.send_to_queue_async("xucode_key_set", ...args);
|
|
74
74
|
};
|
|
75
75
|
|
|
76
|
-
export const
|
|
77
|
-
broker.send_to_queue_async("
|
|
76
|
+
export const xucode_key_list = function (...args) {
|
|
77
|
+
broker.send_to_queue_async("xucode_key_list", ...args);
|
|
78
78
|
};
|
|
79
79
|
|
|
80
|
-
export const
|
|
81
|
-
broker.send_to_queue_async("
|
|
80
|
+
export const xucode_key_delete = function (...args) {
|
|
81
|
+
broker.send_to_queue_async("xucode_key_delete", ...args);
|
|
82
82
|
};
|
|
83
83
|
|
|
84
|
-
export const
|
|
85
|
-
broker.send_to_queue_async("
|
|
84
|
+
export const xucode_engine_list = function (...args) {
|
|
85
|
+
broker.send_to_queue_async("xucode_engine_list", ...args);
|
|
86
86
|
};
|
|
87
87
|
|
|
88
88
|
export const git_repo_disconnect = function (...args) {
|
|
89
89
|
broker.send_to_queue_async("git_repo_disconnect", ...args);
|
|
90
90
|
};
|
|
91
91
|
|
|
92
|
-
export const
|
|
93
|
-
broker.send_to_queue_async("
|
|
92
|
+
export const xucode_run = function (...args) {
|
|
93
|
+
broker.send_to_queue_async("xucode_run", ...args);
|
|
94
94
|
};
|
|
95
95
|
|
|
96
96
|
export const git_repo_status = function (...args) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
//
|
|
1
|
+
// Xucode engines: the seam between "an agent did some work" and "which CLI did it".
|
|
2
2
|
//
|
|
3
|
-
// docs/plans/
|
|
3
|
+
// docs/plans/xucode.md 5.1. An engine is four things and no more:
|
|
4
4
|
//
|
|
5
5
|
// 1. AUTH what environment it needs to be allowed to run
|
|
6
6
|
// 2. A LAUNCH the argv, including how to resume an existing thread
|
|
@@ -41,7 +41,7 @@ if (!global._conf) {
|
|
|
41
41
|
global._conf = (await import(path.join(process.env.XUDA_HOME, 'common', 'load_conf.mjs'))).loadConf();
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
-
const engines_conf = () => global._conf.
|
|
44
|
+
const engines_conf = () => global._conf.xucode?.engines || {};
|
|
45
45
|
|
|
46
46
|
const num = (...values) => {
|
|
47
47
|
for (const v of values) {
|
|
@@ -132,6 +132,48 @@ const codex_adapter = {
|
|
|
132
132
|
return api_key ? { OPENAI_API_KEY: api_key } : {};
|
|
133
133
|
},
|
|
134
134
|
|
|
135
|
+
// Point codex at the region-server proxy instead of the provider, so the machine holds a
|
|
136
|
+
// run token rather than a key. See xucode_proxy.mjs for why that trade is worth making.
|
|
137
|
+
//
|
|
138
|
+
// OPENAI_BASE_URL DOES NOT WORK for this, which is worth stating because it is the obvious
|
|
139
|
+
// thing to reach for: codex 0.128.0 ignores it outright. Verified against the real binary by
|
|
140
|
+
// pointing it at a listener that recorded nothing while the turn completed against the live
|
|
141
|
+
// API. The supported redirect is a custom model_provider, whose `env_key` names the variable
|
|
142
|
+
// codex puts in the Authorization header, which is exactly the slot our token wants.
|
|
143
|
+
proxy_launch({ base_url, token, home } = {}) {
|
|
144
|
+
const config = [
|
|
145
|
+
'# Written by Xucode for this run. The token below is a run-scoped credential for the',
|
|
146
|
+
'# Xuda proxy, not a provider key: it is signed, it expires with the run, and it is only',
|
|
147
|
+
'# accepted by this one endpoint.',
|
|
148
|
+
'model_provider = "xucode"',
|
|
149
|
+
'',
|
|
150
|
+
'# `codex exec` sandboxes to READ-ONLY by default, so without this the agent reads the',
|
|
151
|
+
'# project, decides on a change, cannot write it, and reports success over an unchanged',
|
|
152
|
+
'# repository. Verified the hard way: a run that made three model calls and produced no',
|
|
153
|
+
'# files. workspace-write is the right level rather than a full bypass, because it is also',
|
|
154
|
+
'# correct when the substrate is the region server itself, where "the machine is the',
|
|
155
|
+
'# sandbox" is not true.',
|
|
156
|
+
'sandbox_mode = "workspace-write"',
|
|
157
|
+
'# Nothing is watching to approve anything: a prompt here is a run that hangs until it',
|
|
158
|
+
'# times out.',
|
|
159
|
+
'approval_policy = "never"',
|
|
160
|
+
'',
|
|
161
|
+
'[model_providers.xucode]',
|
|
162
|
+
'name = "Xucode"',
|
|
163
|
+
`base_url = "${base_url}/v1"`,
|
|
164
|
+
'env_key = "XUCODE_TOKEN"',
|
|
165
|
+
'wire_api = "responses"',
|
|
166
|
+
'',
|
|
167
|
+
].join('\n');
|
|
168
|
+
return {
|
|
169
|
+
// CODEX_HOME moves the whole config directory, which also guarantees the machine cannot
|
|
170
|
+
// fall back to a stored auth.json: there is nothing in a fresh home to fall back to.
|
|
171
|
+
env: { CODEX_HOME: home, XUCODE_TOKEN: token },
|
|
172
|
+
files: [{ path: `${home}/config.toml`, content: config }],
|
|
173
|
+
scratch: home,
|
|
174
|
+
};
|
|
175
|
+
},
|
|
176
|
+
|
|
135
177
|
normalize(event) {
|
|
136
178
|
if (!event || typeof event !== 'object') return [];
|
|
137
179
|
switch (event.type) {
|
|
@@ -190,6 +232,15 @@ const claude_code_adapter = {
|
|
|
190
232
|
return api_key ? { ANTHROPIC_API_KEY: api_key } : {};
|
|
191
233
|
},
|
|
192
234
|
|
|
235
|
+
// The same seam, for the day BYOK keys move behind the proxy too. Claude Code reads
|
|
236
|
+
// ANTHROPIC_BASE_URL and takes its credential from ANTHROPIC_AUTH_TOKEN, so no config file
|
|
237
|
+
// is needed. UNVERIFIED against the real CLI, like the rest of this adapter: nothing routes
|
|
238
|
+
// through here yet, because a customer's own key on a customer's own machine is not the
|
|
239
|
+
// problem the proxy was built to solve.
|
|
240
|
+
proxy_launch({ base_url, token } = {}) {
|
|
241
|
+
return { env: { ANTHROPIC_BASE_URL: base_url, ANTHROPIC_AUTH_TOKEN: token }, files: [] };
|
|
242
|
+
},
|
|
243
|
+
|
|
193
244
|
normalize(event) {
|
|
194
245
|
if (!event || typeof event !== 'object') return [];
|
|
195
246
|
switch (event.type) {
|
|
@@ -255,21 +306,37 @@ export const get_engine = function (key) {
|
|
|
255
306
|
// A BYOK engine with no key is refused HERE, before anything is spawned. The alternative is
|
|
256
307
|
// a run that starts, burns a workspace slot, and dies on a provider 401 that reads to the
|
|
257
308
|
// customer like our failure.
|
|
258
|
-
export const prepare_launch = function ({ engine: engine_key, model, resume_session_id, api_key, sandbox, bypass_sandbox } = {}) {
|
|
309
|
+
export const prepare_launch = function ({ engine: engine_key, model, resume_session_id, api_key, sandbox, bypass_sandbox, proxy = null } = {}) {
|
|
259
310
|
const engine = get_engine(engine_key);
|
|
260
311
|
if (!engine) return { error: 'That engine is not available.' };
|
|
261
|
-
|
|
312
|
+
// A proxied launch satisfies the credential requirement without a key being present HERE: the
|
|
313
|
+
// customer's key stays on the region server and the proxy attaches it. The caller is responsible
|
|
314
|
+
// for only minting a token once it knows the account actually has a key, which it checks without
|
|
315
|
+
// reading one.
|
|
316
|
+
if (engine.byok && !api_key && !proxy) {
|
|
262
317
|
return {
|
|
263
318
|
error: `${engine.label} runs on your own API key. Add an ${engine.provider === 'anthropic' ? 'Anthropic' : engine.provider} API key in settings, then try again.`,
|
|
264
319
|
needs_key: engine.provider,
|
|
265
320
|
};
|
|
266
321
|
}
|
|
322
|
+
|
|
323
|
+
// The proxy REPLACES the credential rather than sitting alongside it. Leaving a real key in
|
|
324
|
+
// the environment as well would defeat the point twice over: the engine would prefer the key
|
|
325
|
+
// and skip the proxy, and the key would be on the machine anyway.
|
|
326
|
+
const proxied = proxy && typeof engine.proxy_launch === 'function' ? engine.proxy_launch(proxy) : null;
|
|
327
|
+
|
|
267
328
|
return {
|
|
268
329
|
engine: engine.key,
|
|
269
330
|
label: engine.label,
|
|
270
331
|
command: engine.command,
|
|
271
332
|
args: engine.build_args({ model, resume_session_id, sandbox, bypass_sandbox }),
|
|
272
|
-
env: engine.env({ api_key }),
|
|
333
|
+
env: proxied ? { ...engine.env({}), ...proxied.env } : engine.env({ api_key }),
|
|
334
|
+
// Files the machine needs before the engine starts. The runner writes them without knowing
|
|
335
|
+
// what they are, which keeps "codex needs a TOML" from becoming everyone's problem.
|
|
336
|
+
files: proxied?.files || [],
|
|
337
|
+
// Where those files went, so the runner can remove them without knowing what they were.
|
|
338
|
+
scratch_dir: proxied?.scratch || null,
|
|
339
|
+
proxied: !!proxied,
|
|
273
340
|
resuming: !!resume_session_id,
|
|
274
341
|
};
|
|
275
342
|
};
|
|
@@ -310,7 +377,7 @@ export const create_run_state = function () {
|
|
|
310
377
|
case 'error':
|
|
311
378
|
// Verbatim, per plan 5.2. With BYOK we cannot see the customer's balance or rate
|
|
312
379
|
// limits, so a 429 from their account must read as their provider saying no, not
|
|
313
|
-
// as
|
|
380
|
+
// as Xucode breaking.
|
|
314
381
|
state.error = event.message;
|
|
315
382
|
break;
|
|
316
383
|
case 'done':
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
//
|
|
1
|
+
// Xucode git mirror: how a repository reaches a machine we do not fully trust.
|
|
2
2
|
//
|
|
3
|
-
// docs/plans/
|
|
3
|
+
// docs/plans/xucode.md 9.2. UI-226's verified property is that a repository token never leaves the
|
|
4
4
|
// region server: it is validated by an authenticated ls-remote before anything is stored, kept on a
|
|
5
5
|
// git_repo doc in the account's project database rather than in xuda_master (which replicates
|
|
6
6
|
// fleet-wide), and handed to git only through GIT_ASKPASS so it never lands in an argv, in
|
|
@@ -28,6 +28,7 @@
|
|
|
28
28
|
// default.
|
|
29
29
|
|
|
30
30
|
import fs from 'fs';
|
|
31
|
+
import os from 'os';
|
|
31
32
|
import path from 'node:path';
|
|
32
33
|
|
|
33
34
|
const DEFAULTS = {
|
|
@@ -66,7 +67,7 @@ export const create_mirror = function ({ git_exec, du_mb, mirror_root, clone_tim
|
|
|
66
67
|
await fs.promises.rm(dir, { recursive: true, force: true }).catch(() => {});
|
|
67
68
|
const cloned = await git_exec({ args: ['clone', '--bare', '--no-tags', repo.remote_url, dir], token, timeout_ms: CLONE_TIMEOUT_MS });
|
|
68
69
|
if (cloned.exit_code !== 0) {
|
|
69
|
-
console.error(`[
|
|
70
|
+
console.error(`[xucode] mirror clone failed for ${repo._id}: exit ${cloned.exit_code}`);
|
|
70
71
|
return { error: 'Xuda could not clone that repository.' };
|
|
71
72
|
}
|
|
72
73
|
// A bare clone carries no fetch refspec, so without this the mirror would never learn about a
|
|
@@ -83,7 +84,7 @@ export const create_mirror = function ({ git_exec, du_mb, mirror_root, clone_tim
|
|
|
83
84
|
const fetched = await git_exec({ cwd: dir, args: ['fetch', '--prune', 'origin'], token, timeout_ms: CLONE_TIMEOUT_MS });
|
|
84
85
|
// A stale mirror still serves a clone, so a failed refresh is a warning rather than an error:
|
|
85
86
|
// the customer gets slightly old code instead of no code, and the next run tries again.
|
|
86
|
-
if (fetched.exit_code !== 0) console.warn(`[
|
|
87
|
+
if (fetched.exit_code !== 0) console.warn(`[xucode] mirror fetch failed for ${repo._id}: exit ${fetched.exit_code}`);
|
|
87
88
|
return { dir, created: false, stale: fetched.exit_code !== 0 };
|
|
88
89
|
},
|
|
89
90
|
|
|
@@ -94,7 +95,7 @@ export const create_mirror = function ({ git_exec, du_mb, mirror_root, clone_tim
|
|
|
94
95
|
await fs.promises.rm(dest, { recursive: true, force: true }).catch(() => {});
|
|
95
96
|
const cloned = await git_exec({ args: ['clone', '--no-tags', from, dest], timeout_ms: CLONE_TIMEOUT_MS });
|
|
96
97
|
if (cloned.exit_code !== 0) {
|
|
97
|
-
console.error(`[
|
|
98
|
+
console.error(`[xucode] workspace clone from mirror failed: exit ${cloned.exit_code}`);
|
|
98
99
|
return { error: 'Xuda could not prepare a working copy for this project.' };
|
|
99
100
|
}
|
|
100
101
|
|
|
@@ -128,7 +129,7 @@ export const create_mirror = function ({ git_exec, du_mb, mirror_root, clone_tim
|
|
|
128
129
|
if (!has_git) return await this.clone_workspace({ mirror_dir: from, dest, branch, default_branch });
|
|
129
130
|
|
|
130
131
|
const fetched = await git_exec({ cwd: dest, args: ['fetch', '--prune', 'origin'], timeout_ms: CLONE_TIMEOUT_MS });
|
|
131
|
-
if (fetched.exit_code !== 0) console.warn(`[
|
|
132
|
+
if (fetched.exit_code !== 0) console.warn(`[xucode] workspace fetch from mirror failed: exit ${fetched.exit_code}`);
|
|
132
133
|
|
|
133
134
|
if (branch) {
|
|
134
135
|
// Reuse the branch when it is already here rather than resetting onto the remote: a
|
|
@@ -153,13 +154,119 @@ export const create_mirror = function ({ git_exec, du_mb, mirror_root, clone_tim
|
|
|
153
154
|
return { dir: dest, branch: branch || null, base_sha: head.exit_code === 0 ? head.stdout : null, reused: true };
|
|
154
155
|
},
|
|
155
156
|
|
|
157
|
+
// ── Getting the repository INTO a workspace that cannot reach the mirror ────────────────
|
|
158
|
+
// The mirror is a bare repo on the region server's disk. A Xucode machine is a different
|
|
159
|
+
// computer, keyless by design and reachable only through the Proxmox guest agent, so it cannot
|
|
160
|
+
// clone from a path it cannot see and must not be handed a key so that it could.
|
|
161
|
+
//
|
|
162
|
+
// A git BUNDLE is the answer git already has for this: one file carrying real objects and refs,
|
|
163
|
+
// clonable and fetchable like any remote, and needing no network path between the two machines.
|
|
164
|
+
// It crosses as base64 text, which is the only thing the agent channel carries.
|
|
165
|
+
//
|
|
166
|
+
// The cost is honest and worth stating: the first hydration of a large repository moves the
|
|
167
|
+
// whole history as base64 in 60 KB chunks, which is slow. It is correct everywhere today with
|
|
168
|
+
// no new infrastructure and no credentials on the machine. Making the mirror reachable over the
|
|
169
|
+
// WireGuard mesh would turn this into a plain clone and is the optimisation, not the design.
|
|
170
|
+
async hydrate_workspace({ workspace, uid, repo, branch, default_branch }) {
|
|
171
|
+
const dir = mirror_dir(uid, repo._id);
|
|
172
|
+
const bundle = path.join(os.tmpdir(), `xucode-${repo._id}-${Date.now()}.bundle`);
|
|
173
|
+
try {
|
|
174
|
+
// --all rather than one branch: the workspace needs the default branch to cut from as well
|
|
175
|
+
// as any branch this chat already has.
|
|
176
|
+
const made = await git_exec({ cwd: dir, args: ['bundle', 'create', bundle, '--all'], timeout_ms: CLONE_TIMEOUT_MS });
|
|
177
|
+
if (made.exit_code !== 0) return { error: 'Xuda could not package that repository for the machine.' };
|
|
178
|
+
const b64 = await fs.promises.readFile(bundle, 'base64');
|
|
179
|
+
|
|
180
|
+
// Does the workspace already hold a checkout? A warm machine keeps its working copy between
|
|
181
|
+
// runs, and that warmth is most of what the paid tiers sell, so this fetches rather than
|
|
182
|
+
// re-cloning and never touches uncommitted work.
|
|
183
|
+
const existing = await workspace.exec(['sh', '-c', 'test -d .git && echo yes || echo no'], { timeout_ms: 60000 });
|
|
184
|
+
const has_repo = (existing.stdout || '').includes('yes');
|
|
185
|
+
|
|
186
|
+
await workspace.write_file('.xucode_repo.b64', b64);
|
|
187
|
+
// GNU base64 decodes with -d, BSD (macOS) with -D and has no --decode. A workspace can be
|
|
188
|
+
// either: the VM is Linux, a developer running the local substrate is not. Same shape of
|
|
189
|
+
// trap as setsid in the preview, and it only ever shows up on the machine you are not on.
|
|
190
|
+
// Read from STDIN, not a positional filename. GNU base64 accepts a path, BSD (macOS) does
|
|
191
|
+
// not and answers "invalid argument" while still creating the empty output file, so the
|
|
192
|
+
// failure looks like a corrupt bundle rather than a wrong flag. Redirection works on both,
|
|
193
|
+
// and so does -d on both, which the -D fallback obscured.
|
|
194
|
+
const unpack = await workspace.exec(['sh', '-c', 'base64 -d < .xucode_repo.b64 > .xucode_repo.bundle && rm -f .xucode_repo.b64'], { timeout_ms: CLONE_TIMEOUT_MS });
|
|
195
|
+
if (unpack.exit_code !== 0) return { error: 'Xuda could not unpack that repository on the machine.' };
|
|
196
|
+
|
|
197
|
+
if (!has_repo) {
|
|
198
|
+
// `git clone <bundle> .` refuses a non-empty directory, so init and fetch instead, which
|
|
199
|
+
// also leaves the bundle as a named remote we control.
|
|
200
|
+
const init = await workspace.exec(['sh', '-c', 'git init -q . && git remote remove origin 2>/dev/null; git remote add origin .xucode_repo.bundle'], { timeout_ms: 120000 });
|
|
201
|
+
if (init.exit_code !== 0) return { error: 'Xuda could not prepare a working copy on the machine.' };
|
|
202
|
+
} else {
|
|
203
|
+
await workspace.exec(['sh', '-c', 'git remote remove origin 2>/dev/null; git remote add origin .xucode_repo.bundle'], { timeout_ms: 60000 });
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const fetched = await workspace.exec(['sh', '-c', 'git fetch -q origin "+refs/heads/*:refs/remotes/origin/*"'], { timeout_ms: CLONE_TIMEOUT_MS });
|
|
207
|
+
if (fetched.exit_code !== 0) {
|
|
208
|
+
console.error(`[xucode] workspace fetch from bundle failed: ${(fetched.stderr || '').slice(-200)}`);
|
|
209
|
+
return { error: 'Xuda could not read that repository on the machine.' };
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
if (branch) {
|
|
213
|
+
// Same contract as everywhere else: the chat owns a branch, the default branch is never
|
|
214
|
+
// checked out for writing. Reuse the branch when the workspace already has it rather than
|
|
215
|
+
// resetting onto the remote, or a warm machine loses whatever the last run left dirty.
|
|
216
|
+
const local = await workspace.exec(['sh', '-c', `git rev-parse --verify refs/heads/${branch}`], { timeout_ms: 60000 });
|
|
217
|
+
if (local.exit_code === 0) {
|
|
218
|
+
const co = await workspace.exec(['sh', '-c', `git checkout -q ${branch}`], { timeout_ms: 120000 });
|
|
219
|
+
if (co.exit_code !== 0) return { error: 'Xuda could not switch to this chat branch on the machine.' };
|
|
220
|
+
} else {
|
|
221
|
+
const on_remote = await workspace.exec(['sh', '-c', `git rev-parse --verify refs/remotes/origin/${branch}`], { timeout_ms: 60000 });
|
|
222
|
+
const from = on_remote.exit_code === 0 ? `origin/${branch}` : `origin/${default_branch}`;
|
|
223
|
+
const made_branch = await workspace.exec(['sh', '-c', `git checkout -q -B ${branch} ${from} || git checkout -q -B ${branch}`], { timeout_ms: 120000 });
|
|
224
|
+
if (made_branch.exit_code !== 0) return { error: 'Xuda could not create a branch on the machine.' };
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
const head = await workspace.exec(['sh', '-c', 'git rev-parse HEAD'], { timeout_ms: 60000 });
|
|
229
|
+
return { dir: workspace.dir, branch: branch || null, base_sha: (head.stdout || '').trim() || null, reused: has_repo };
|
|
230
|
+
} finally {
|
|
231
|
+
await fs.promises.rm(bundle, { force: true }).catch(() => {});
|
|
232
|
+
}
|
|
233
|
+
},
|
|
234
|
+
|
|
235
|
+
// The return trip. The workspace bundles whatever the run produced, the bundle crosses back as
|
|
236
|
+
// base64, and the MIRROR fetches from it. Only then does the mirror push upstream, which is the
|
|
237
|
+
// one authenticated step and it still happens on the region server.
|
|
238
|
+
async collect_from_workspace({ workspace, uid, repo, branch }) {
|
|
239
|
+
if (!branch) return { error: 'no branch to collect' };
|
|
240
|
+
// -w0 is GNU-only, and BSD wraps at 76 columns with no way to turn it off, so the newlines
|
|
241
|
+
// are stripped afterwards instead. Works on both.
|
|
242
|
+
const made = await workspace.exec(['sh', '-c', `git bundle create .xucode_out.bundle ${branch} && base64 < .xucode_out.bundle | tr -d '\n'`], { timeout_ms: CLONE_TIMEOUT_MS });
|
|
243
|
+
if (made.exit_code !== 0) {
|
|
244
|
+
console.error(`[xucode] could not bundle ${branch} on the machine: ${(made.stderr || '').slice(-200)}`);
|
|
245
|
+
return { error: 'Xuda could not package your changes.' };
|
|
246
|
+
}
|
|
247
|
+
const bundle = path.join(os.tmpdir(), `xucode-in-${repo._id}-${Date.now()}.bundle`);
|
|
248
|
+
try {
|
|
249
|
+
await fs.promises.writeFile(bundle, Buffer.from((made.stdout || '').trim(), 'base64'));
|
|
250
|
+
const dir = mirror_dir(uid, repo._id);
|
|
251
|
+
const fetched = await git_exec({ cwd: dir, args: ['fetch', bundle, `+refs/heads/${branch}:refs/heads/${branch}`], timeout_ms: CLONE_TIMEOUT_MS });
|
|
252
|
+
if (fetched.exit_code !== 0) {
|
|
253
|
+
console.error(`[xucode] mirror could not fetch ${branch} from the workspace bundle: exit ${fetched.exit_code}`);
|
|
254
|
+
return { error: 'Xuda could not save your changes.' };
|
|
255
|
+
}
|
|
256
|
+
return { collected: true, branch };
|
|
257
|
+
} finally {
|
|
258
|
+
await fs.promises.rm(bundle, { force: true }).catch(() => {});
|
|
259
|
+
await workspace.exec(['sh', '-c', 'rm -f .xucode_out.bundle'], { timeout_ms: 60000 }).catch(() => {});
|
|
260
|
+
}
|
|
261
|
+
},
|
|
262
|
+
|
|
156
263
|
// Workspace to mirror. Runs with the workspace's own git, over a path or a VPN url, and never
|
|
157
264
|
// sees a credential.
|
|
158
265
|
async push_from_workspace({ work_dir, mirror_dir: to, branch }) {
|
|
159
266
|
if (!branch) return { error: 'no branch to push' };
|
|
160
267
|
const pushed = await git_exec({ cwd: work_dir, args: ['push', to, `refs/heads/${branch}:refs/heads/${branch}`], timeout_ms: CLONE_TIMEOUT_MS });
|
|
161
268
|
if (pushed.exit_code !== 0) {
|
|
162
|
-
console.error(`[
|
|
269
|
+
console.error(`[xucode] workspace push to mirror failed for ${branch}: exit ${pushed.exit_code}`);
|
|
163
270
|
return { error: 'Xuda could not save that branch.' };
|
|
164
271
|
}
|
|
165
272
|
return { pushed: true, branch };
|
|
@@ -184,7 +291,7 @@ export const create_mirror = function ({ git_exec, du_mb, mirror_root, clone_tim
|
|
|
184
291
|
timeout_ms: CLONE_TIMEOUT_MS,
|
|
185
292
|
});
|
|
186
293
|
if (pushed.exit_code !== 0) {
|
|
187
|
-
console.error(`[
|
|
294
|
+
console.error(`[xucode] mirror push failed for ${repo._id} ${branch}: exit ${pushed.exit_code}`);
|
|
188
295
|
return { error: 'Xuda could not push that branch to your repository.' };
|
|
189
296
|
}
|
|
190
297
|
return { pushed: true, branch };
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
//
|
|
1
|
+
// Xucode preview: the project, actually running.
|
|
2
2
|
//
|
|
3
|
-
// docs/plans/
|
|
3
|
+
// docs/plans/xucode.md section 7, and the last gate of the verify loop in section 6. The verify
|
|
4
4
|
// loop already left the seam open (`runtime_check`); this is what plugs into it.
|
|
5
5
|
//
|
|
6
6
|
// ── Why this is a gate and not a feature ───────────────────────────────────────────────
|
|
@@ -27,7 +27,7 @@ if (!global._conf) {
|
|
|
27
27
|
global._conf = (await import(path.join(process.env.XUDA_HOME, 'common', 'load_conf.mjs'))).loadConf();
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
-
const preview_conf = () => global._conf.
|
|
30
|
+
const preview_conf = () => global._conf.xucode?.preview || {};
|
|
31
31
|
const num = (key, fallback) => Number(preview_conf()[key]) || fallback;
|
|
32
32
|
|
|
33
33
|
// Where a project's dev server is told to listen. Fixed per project rather than random, so a
|
|
@@ -111,8 +111,8 @@ export const extract_errors = function (log, max = num('max_errors', 8)) {
|
|
|
111
111
|
const shq = (value) => `'${String(value).replace(/'/g, `'\\''`)}'`;
|
|
112
112
|
|
|
113
113
|
export const create_preview = function ({ conf = preview_conf, now = () => Date.now() } = {}) {
|
|
114
|
-
const log_path = (project_id) => `/tmp/
|
|
115
|
-
const pid_path = (project_id) => `/tmp/
|
|
114
|
+
const log_path = (project_id) => `/tmp/xucode-preview-${String(project_id).replace(/[^a-zA-Z0-9_-]/g, '')}.log`;
|
|
115
|
+
const pid_path = (project_id) => `/tmp/xucode-preview-${String(project_id).replace(/[^a-zA-Z0-9_-]/g, '')}.pid`;
|
|
116
116
|
|
|
117
117
|
const is_listening = async function (workspace, port) {
|
|
118
118
|
// Node is guaranteed present (this is a Node project by definition) and curl is not, so the
|