office-core 0.1.4 → 0.2.0
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/.runtime-dist/scripts/build-windows-release.js +54 -0
- package/.runtime-dist/scripts/home-agent-host.js +9 -18
- package/.runtime-dist/scripts/host-config-command.js +33 -0
- package/.runtime-dist/scripts/host-doctor.js +18 -13
- package/.runtime-dist/scripts/host-login.js +13 -11
- package/.runtime-dist/scripts/host-logs.js +22 -0
- package/.runtime-dist/scripts/host-menu.js +1 -1
- package/.runtime-dist/scripts/host-open.js +7 -10
- package/.runtime-dist/scripts/host-support-bundle.js +43 -0
- package/.runtime-dist/scripts/install-host.js +88 -111
- package/.runtime-dist/scripts/lib/cli-support.js +199 -0
- package/.runtime-dist/scripts/lib/host-config.js +91 -57
- package/.runtime-dist/scripts/lib/local-runner.js +16 -3
- package/.runtime-dist/scripts/office-cli.js +154 -223
- package/README.md +57 -94
- package/bin/double-penetration-host.mjs +1 -82
- package/bin/office-core.mjs +95 -0
- package/package.json +11 -7
- package/public/index.html +373 -1
- package/public/install-host.ps1 +7 -5
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
import { appendFile, cp, mkdir, mkdtemp, readFile, readdir, rm, stat, writeFile } from 'node:fs/promises';
|
|
2
|
+
import { existsSync } from 'node:fs';
|
|
3
|
+
import os from 'node:os';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import process from 'node:process';
|
|
6
|
+
import { spawn, spawnSync } from 'node:child_process';
|
|
7
|
+
export function parseCliArgs(argv, options = {}) {
|
|
8
|
+
const bools = new Set(options.boolean ?? []);
|
|
9
|
+
const values = new Set(options.value ?? []);
|
|
10
|
+
const aliases = options.aliases ?? {};
|
|
11
|
+
const out = {};
|
|
12
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
13
|
+
const item = argv[i];
|
|
14
|
+
if (!item.startsWith('--')) {
|
|
15
|
+
throw new Error(`Unexpected positional argument: ${item}`);
|
|
16
|
+
}
|
|
17
|
+
const rawKey = item.slice(2);
|
|
18
|
+
const key = aliases[rawKey] ?? rawKey;
|
|
19
|
+
if (!bools.has(key) && !values.has(key)) {
|
|
20
|
+
throw new Error(`Unknown option: --${rawKey}`);
|
|
21
|
+
}
|
|
22
|
+
if (bools.has(key)) {
|
|
23
|
+
const next = argv[i + 1];
|
|
24
|
+
if (next && !next.startsWith('--')) {
|
|
25
|
+
out[key] = next;
|
|
26
|
+
i += 1;
|
|
27
|
+
}
|
|
28
|
+
else {
|
|
29
|
+
out[key] = 'true';
|
|
30
|
+
}
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
const next = argv[i + 1];
|
|
34
|
+
if (!next || next.startsWith('--')) {
|
|
35
|
+
throw new Error(`Missing value for --${rawKey}`);
|
|
36
|
+
}
|
|
37
|
+
out[key] = next;
|
|
38
|
+
i += 1;
|
|
39
|
+
}
|
|
40
|
+
return out;
|
|
41
|
+
}
|
|
42
|
+
export function parseBooleanLike(value, fallback) {
|
|
43
|
+
if (value == null || value === '') {
|
|
44
|
+
return fallback;
|
|
45
|
+
}
|
|
46
|
+
const normalized = value.trim().toLowerCase();
|
|
47
|
+
if (['1', 'true', 'yes', 'y', 'on'].includes(normalized)) {
|
|
48
|
+
return true;
|
|
49
|
+
}
|
|
50
|
+
if (['0', 'false', 'no', 'n', 'off'].includes(normalized)) {
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
53
|
+
throw new Error(`Invalid boolean value: ${value}`);
|
|
54
|
+
}
|
|
55
|
+
export function parseNumberLike(value, fallback, label = 'number') {
|
|
56
|
+
if (value == null || value === '') {
|
|
57
|
+
return fallback;
|
|
58
|
+
}
|
|
59
|
+
const parsed = Number(value);
|
|
60
|
+
if (!Number.isFinite(parsed)) {
|
|
61
|
+
throw new Error(`Invalid ${label}: ${value}`);
|
|
62
|
+
}
|
|
63
|
+
return parsed;
|
|
64
|
+
}
|
|
65
|
+
export async function openExternal(target) {
|
|
66
|
+
const command = process.platform === 'win32'
|
|
67
|
+
? 'cmd.exe'
|
|
68
|
+
: process.platform === 'darwin'
|
|
69
|
+
? 'open'
|
|
70
|
+
: 'xdg-open';
|
|
71
|
+
const args = process.platform === 'win32' ? ['/c', 'start', '', target] : [target];
|
|
72
|
+
await new Promise((resolve, reject) => {
|
|
73
|
+
const child = spawn(command, args, {
|
|
74
|
+
detached: true,
|
|
75
|
+
stdio: 'ignore',
|
|
76
|
+
windowsHide: true,
|
|
77
|
+
});
|
|
78
|
+
child.on('error', reject);
|
|
79
|
+
child.unref();
|
|
80
|
+
resolve();
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
export function escapeBatch(value) {
|
|
84
|
+
return value.replace(/"/g, '""');
|
|
85
|
+
}
|
|
86
|
+
export function buildStartupLauncher(rootDir) {
|
|
87
|
+
const runtimeEntry = path.join(rootDir, 'bin', 'office-core.mjs');
|
|
88
|
+
return [
|
|
89
|
+
'@echo off',
|
|
90
|
+
'setlocal',
|
|
91
|
+
`cd /d "${escapeBatch(rootDir)}"`,
|
|
92
|
+
'where node >nul 2>nul',
|
|
93
|
+
'if errorlevel 1 (',
|
|
94
|
+
' echo Node.js was not found on PATH.',
|
|
95
|
+
' exit /b 1',
|
|
96
|
+
')',
|
|
97
|
+
`start "office core" cmd /c "cd /d ""${escapeBatch(rootDir)}"" && node ""${escapeBatch(runtimeEntry)}"" start"`,
|
|
98
|
+
'endlocal',
|
|
99
|
+
'',
|
|
100
|
+
].join('\r\n');
|
|
101
|
+
}
|
|
102
|
+
export function buildDesktopLauncher(rootDir) {
|
|
103
|
+
const runtimeEntry = path.join(rootDir, 'bin', 'office-core.mjs');
|
|
104
|
+
return [
|
|
105
|
+
'@echo off',
|
|
106
|
+
'setlocal',
|
|
107
|
+
`cd /d "${escapeBatch(rootDir)}"`,
|
|
108
|
+
`node "${escapeBatch(runtimeEntry)}"`,
|
|
109
|
+
'if errorlevel 1 pause',
|
|
110
|
+
'endlocal',
|
|
111
|
+
'',
|
|
112
|
+
].join('\r\n');
|
|
113
|
+
}
|
|
114
|
+
export function buildDashboardLauncher(rootDir) {
|
|
115
|
+
const runtimeEntry = path.join(rootDir, 'bin', 'office-core.mjs');
|
|
116
|
+
return [
|
|
117
|
+
'@echo off',
|
|
118
|
+
'setlocal',
|
|
119
|
+
`cd /d "${escapeBatch(rootDir)}"`,
|
|
120
|
+
`node "${escapeBatch(runtimeEntry)}" open`,
|
|
121
|
+
'if errorlevel 1 pause',
|
|
122
|
+
'endlocal',
|
|
123
|
+
'',
|
|
124
|
+
].join('\r\n');
|
|
125
|
+
}
|
|
126
|
+
export async function appendLogLine(logPath, line) {
|
|
127
|
+
await mkdir(path.dirname(logPath), { recursive: true });
|
|
128
|
+
await appendFile(logPath, `${new Date().toISOString()} ${line}\n`, 'utf8');
|
|
129
|
+
}
|
|
130
|
+
export async function readRecentLines(logPath, maxLines = 120) {
|
|
131
|
+
try {
|
|
132
|
+
const raw = await readFile(logPath, 'utf8');
|
|
133
|
+
return raw.split(/\r?\n/).filter(Boolean).slice(-maxLines);
|
|
134
|
+
}
|
|
135
|
+
catch {
|
|
136
|
+
return [];
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
export async function createSupportBundle(params) {
|
|
140
|
+
const stamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
141
|
+
const baseName = `${params.bundleNamePrefix}-${stamp}`;
|
|
142
|
+
const stagingRoot = await mkdtemp(path.join(os.tmpdir(), `${params.bundleNamePrefix}-`));
|
|
143
|
+
const bundleDir = path.join(stagingRoot, baseName);
|
|
144
|
+
await mkdir(bundleDir, { recursive: true });
|
|
145
|
+
await writeFile(path.join(bundleDir, 'bundle-metadata.json'), JSON.stringify(params.metadata, null, 2), 'utf8');
|
|
146
|
+
for (const entry of params.includePaths) {
|
|
147
|
+
if (!existsSync(entry.source)) {
|
|
148
|
+
continue;
|
|
149
|
+
}
|
|
150
|
+
const destination = path.join(bundleDir, entry.target ?? path.basename(entry.source));
|
|
151
|
+
await mkdir(path.dirname(destination), { recursive: true });
|
|
152
|
+
await cp(entry.source, destination, { recursive: true, force: true });
|
|
153
|
+
}
|
|
154
|
+
await mkdir(params.outputRoot, { recursive: true });
|
|
155
|
+
if (process.platform === 'win32') {
|
|
156
|
+
const zipPath = path.join(params.outputRoot, `${baseName}.zip`);
|
|
157
|
+
const compress = spawnSync('powershell.exe', [
|
|
158
|
+
'-NoProfile',
|
|
159
|
+
'-ExecutionPolicy',
|
|
160
|
+
'Bypass',
|
|
161
|
+
'-Command',
|
|
162
|
+
`Compress-Archive -LiteralPath '${bundleDir.replace(/'/g, "''")}' -DestinationPath '${zipPath.replace(/'/g, "''")}' -Force`,
|
|
163
|
+
], { encoding: 'utf8', windowsHide: true });
|
|
164
|
+
if (compress.status === 0 && existsSync(zipPath)) {
|
|
165
|
+
await rm(stagingRoot, { recursive: true, force: true });
|
|
166
|
+
return { outputPath: zipPath, mode: 'zip' };
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
else {
|
|
170
|
+
const zipPath = path.join(params.outputRoot, `${baseName}.zip`);
|
|
171
|
+
const zip = spawnSync('zip', ['-qr', zipPath, baseName], {
|
|
172
|
+
cwd: stagingRoot,
|
|
173
|
+
encoding: 'utf8',
|
|
174
|
+
windowsHide: true,
|
|
175
|
+
});
|
|
176
|
+
if (zip.status === 0 && existsSync(zipPath)) {
|
|
177
|
+
await rm(stagingRoot, { recursive: true, force: true });
|
|
178
|
+
return { outputPath: zipPath, mode: 'zip' };
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
const fallbackDir = path.join(params.outputRoot, baseName);
|
|
182
|
+
await rm(fallbackDir, { recursive: true, force: true }).catch(() => undefined);
|
|
183
|
+
await cp(bundleDir, fallbackDir, { recursive: true, force: true });
|
|
184
|
+
await rm(stagingRoot, { recursive: true, force: true });
|
|
185
|
+
return { outputPath: fallbackDir, mode: 'folder' };
|
|
186
|
+
}
|
|
187
|
+
export async function describePathState(targetPath) {
|
|
188
|
+
try {
|
|
189
|
+
const info = await stat(targetPath);
|
|
190
|
+
if (info.isDirectory()) {
|
|
191
|
+
const items = await readdir(targetPath);
|
|
192
|
+
return `${targetPath} (${items.length} items)`;
|
|
193
|
+
}
|
|
194
|
+
return `${targetPath} (${info.size} bytes)`;
|
|
195
|
+
}
|
|
196
|
+
catch {
|
|
197
|
+
return `${targetPath} (missing)`;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
@@ -1,16 +1,29 @@
|
|
|
1
|
-
import { existsSync } from
|
|
2
|
-
import { mkdir, readFile, rename,
|
|
3
|
-
import os from
|
|
4
|
-
import path from
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
2
|
+
import { appendFile, mkdir, readFile, rename, writeFile } from 'node:fs/promises';
|
|
3
|
+
import os from 'node:os';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import process from 'node:process';
|
|
5
6
|
const HOST_CONFIG_VERSION = 1;
|
|
6
|
-
const HOST_CONFIG_DIRNAME =
|
|
7
|
-
const LEGACY_HOST_CONFIG_DIRNAME =
|
|
8
|
-
const HOST_CONFIG_FILENAME =
|
|
9
|
-
const HOST_SESSIONS_FILENAME =
|
|
10
|
-
const DEFAULT_LOCAL_HOST_TOKEN =
|
|
7
|
+
const HOST_CONFIG_DIRNAME = 'office-host';
|
|
8
|
+
const LEGACY_HOST_CONFIG_DIRNAME = 'double-penetration';
|
|
9
|
+
const HOST_CONFIG_FILENAME = 'host-config.json';
|
|
10
|
+
const HOST_SESSIONS_FILENAME = 'host-sessions.json';
|
|
11
|
+
const DEFAULT_LOCAL_HOST_TOKEN = 'office-host-local-token';
|
|
11
12
|
export function getHostConfigDir() {
|
|
12
13
|
return path.join(baseAppDataDir(), HOST_CONFIG_DIRNAME);
|
|
13
14
|
}
|
|
15
|
+
export function getHostLogsDir() {
|
|
16
|
+
return path.join(getHostConfigDir(), 'logs');
|
|
17
|
+
}
|
|
18
|
+
export function getHostDiagnosticsDir() {
|
|
19
|
+
return path.join(getHostConfigDir(), 'diagnostics');
|
|
20
|
+
}
|
|
21
|
+
export function getSupportBundlesDir() {
|
|
22
|
+
return path.join(getHostDiagnosticsDir(), 'support-bundles');
|
|
23
|
+
}
|
|
24
|
+
export function getHostLogPath() {
|
|
25
|
+
return path.join(getHostLogsDir(), 'office-host.log');
|
|
26
|
+
}
|
|
14
27
|
export function getHostConfigPath() {
|
|
15
28
|
return path.join(getHostConfigDir(), HOST_CONFIG_FILENAME);
|
|
16
29
|
}
|
|
@@ -20,6 +33,10 @@ export function getHostSessionsPath() {
|
|
|
20
33
|
export function getLegacyHostConfigDir() {
|
|
21
34
|
return path.join(baseAppDataDir(), LEGACY_HOST_CONFIG_DIRNAME);
|
|
22
35
|
}
|
|
36
|
+
export async function appendHostLog(message) {
|
|
37
|
+
await mkdir(getHostLogsDir(), { recursive: true });
|
|
38
|
+
await appendFile(getHostLogPath(), `${new Date().toISOString()} ${message}\n`, 'utf8');
|
|
39
|
+
}
|
|
23
40
|
export async function loadHostConfig() {
|
|
24
41
|
const filePath = firstExistingPath([
|
|
25
42
|
path.join(getHostConfigDir(), HOST_CONFIG_FILENAME),
|
|
@@ -28,18 +45,19 @@ export async function loadHostConfig() {
|
|
|
28
45
|
if (!filePath) {
|
|
29
46
|
return null;
|
|
30
47
|
}
|
|
31
|
-
let
|
|
48
|
+
let raw = '';
|
|
32
49
|
try {
|
|
33
|
-
|
|
34
|
-
parsed = parseLooseJson(raw);
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
50
|
+
raw = await readFile(filePath, 'utf8');
|
|
51
|
+
const parsed = parseLooseJson(raw);
|
|
52
|
+
if (!parsed || parsed.version !== HOST_CONFIG_VERSION) {
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
return normalizeConfig(parsed);
|
|
38
56
|
}
|
|
39
|
-
|
|
57
|
+
catch (error) {
|
|
58
|
+
await quarantineCorruptFile(filePath, raw, error);
|
|
40
59
|
return null;
|
|
41
60
|
}
|
|
42
|
-
return normalizeConfig(parsed);
|
|
43
61
|
}
|
|
44
62
|
export async function upsertHostConfig(overrides) {
|
|
45
63
|
const existing = await loadHostConfig();
|
|
@@ -50,8 +68,8 @@ export async function upsertHostConfig(overrides) {
|
|
|
50
68
|
host_id: overrides.host_id ?? existing?.host_id ?? `host_${sanitize(machineName)}_${crypto.randomUUID().slice(0, 8)}`,
|
|
51
69
|
display_name: overrides.display_name ?? existing?.display_name ?? `${machineName} host`,
|
|
52
70
|
machine_name: machineName,
|
|
53
|
-
base_url: overrides.base_url ?? existing?.base_url ??
|
|
54
|
-
project_id: overrides.project_id ?? existing?.project_id ??
|
|
71
|
+
base_url: overrides.base_url ?? existing?.base_url ?? 'http://127.0.0.1:8787',
|
|
72
|
+
project_id: overrides.project_id ?? existing?.project_id ?? 'prj_local',
|
|
55
73
|
workdir: path.resolve(overrides.workdir ?? existing?.workdir ?? process.cwd()),
|
|
56
74
|
token: overrides.token ?? existing?.token ?? DEFAULT_LOCAL_HOST_TOKEN,
|
|
57
75
|
room_cursor_seq: overrides.room_cursor_seq ?? existing?.room_cursor_seq ?? 0,
|
|
@@ -62,6 +80,7 @@ export async function upsertHostConfig(overrides) {
|
|
|
62
80
|
});
|
|
63
81
|
await mkdir(getHostConfigDir(), { recursive: true });
|
|
64
82
|
await writeAtomicJson(getHostConfigPath(), next);
|
|
83
|
+
await appendHostLog(`[config] updated project=${next.project_id} host=${next.host_id} workdir=${next.workdir}`);
|
|
65
84
|
return next;
|
|
66
85
|
}
|
|
67
86
|
export async function loadPersistedHostSessions() {
|
|
@@ -73,16 +92,17 @@ export async function loadPersistedHostSessions() {
|
|
|
73
92
|
return [];
|
|
74
93
|
}
|
|
75
94
|
try {
|
|
76
|
-
const raw = await readFile(filePath,
|
|
95
|
+
const raw = await readFile(filePath, 'utf8');
|
|
77
96
|
const parsed = JSON.parse(raw);
|
|
78
97
|
if (!Array.isArray(parsed)) {
|
|
79
98
|
return [];
|
|
80
99
|
}
|
|
81
100
|
return parsed
|
|
82
|
-
.filter((entry) => entry && typeof entry ===
|
|
101
|
+
.filter((entry) => entry && typeof entry === 'object' && entry.session_id)
|
|
83
102
|
.map((entry) => normalizePersistedSession(entry));
|
|
84
103
|
}
|
|
85
|
-
catch {
|
|
104
|
+
catch (error) {
|
|
105
|
+
await quarantineCorruptFile(filePath, '', error);
|
|
86
106
|
return [];
|
|
87
107
|
}
|
|
88
108
|
}
|
|
@@ -92,20 +112,23 @@ export async function savePersistedHostSessions(sessions) {
|
|
|
92
112
|
await writeAtomicJson(getHostSessionsPath(), next);
|
|
93
113
|
}
|
|
94
114
|
export function getStartupLauncherPath() {
|
|
95
|
-
return path.join(baseAppDataDir(),
|
|
115
|
+
return path.join(baseAppDataDir(), 'Microsoft', 'Windows', 'Start Menu', 'Programs', 'Startup', 'office-host.bat');
|
|
116
|
+
}
|
|
117
|
+
export function getDesktopLauncherPath() {
|
|
118
|
+
return path.join(baseAppDataDir(), 'Microsoft', 'Windows', 'Start Menu', 'Programs', 'Office Core.bat');
|
|
96
119
|
}
|
|
97
120
|
function normalizeConfig(input) {
|
|
98
121
|
return {
|
|
99
122
|
version: HOST_CONFIG_VERSION,
|
|
100
|
-
host_id: String(input.host_id ??
|
|
101
|
-
display_name: String(input.display_name ??
|
|
123
|
+
host_id: String(input.host_id ?? '').trim(),
|
|
124
|
+
display_name: String(input.display_name ?? '').trim(),
|
|
102
125
|
machine_name: String(input.machine_name ?? os.hostname()).trim(),
|
|
103
|
-
base_url: String(input.base_url ??
|
|
104
|
-
project_id: String(input.project_id ??
|
|
126
|
+
base_url: String(input.base_url ?? 'http://127.0.0.1:8787').trim().replace(/\/$/, ''),
|
|
127
|
+
project_id: String(input.project_id ?? 'prj_local').trim(),
|
|
105
128
|
workdir: path.resolve(String(input.workdir ?? process.cwd())),
|
|
106
129
|
token: String(input.token ?? DEFAULT_LOCAL_HOST_TOKEN).trim(),
|
|
107
|
-
room_cursor_seq:
|
|
108
|
-
poll_ms:
|
|
130
|
+
room_cursor_seq: finiteNumber(input.room_cursor_seq, 0),
|
|
131
|
+
poll_ms: finiteNumber(input.poll_ms, 5000),
|
|
109
132
|
auto_start: Boolean(input.auto_start ?? true),
|
|
110
133
|
created_at: String(input.created_at ?? new Date().toISOString()),
|
|
111
134
|
updated_at: String(input.updated_at ?? new Date().toISOString()),
|
|
@@ -113,34 +136,34 @@ function normalizeConfig(input) {
|
|
|
113
136
|
}
|
|
114
137
|
function normalizePersistedSession(input) {
|
|
115
138
|
return {
|
|
116
|
-
session_id: String(input.session_id ??
|
|
139
|
+
session_id: String(input.session_id ?? '').trim(),
|
|
117
140
|
project_id: input.project_id ? String(input.project_id) : null,
|
|
118
|
-
runner: input.runner ===
|
|
119
|
-
agent_id: String(input.agent_id ??
|
|
120
|
-
mode: input.mode ===
|
|
141
|
+
runner: input.runner === 'claude' ? 'claude' : 'codex',
|
|
142
|
+
agent_id: String(input.agent_id ?? '').trim(),
|
|
143
|
+
mode: input.mode === 'execute' || input.mode === 'review' || input.mode === 'attach' ? input.mode : 'brainstorm',
|
|
121
144
|
workdir: path.resolve(String(input.workdir ?? process.cwd())),
|
|
122
|
-
status: input.status ===
|
|
145
|
+
status: input.status === 'completed' || input.status === 'failed' ? input.status : 'running',
|
|
123
146
|
launched_at: String(input.launched_at ?? new Date().toISOString()),
|
|
124
147
|
task_id: input.task_id ? String(input.task_id) : null,
|
|
125
|
-
effort: input.effort ===
|
|
148
|
+
effort: input.effort === 'low' || input.effort === 'medium' || input.effort === 'max' || input.effort === 'high'
|
|
126
149
|
? input.effort
|
|
127
|
-
:
|
|
150
|
+
: 'high',
|
|
128
151
|
shell_pid: Number.isFinite(Number(input.shell_pid)) ? Number(input.shell_pid) : null,
|
|
129
152
|
script_dir: input.script_dir ? String(input.script_dir) : null,
|
|
130
153
|
note: input.note ? String(input.note) : null,
|
|
131
154
|
bootstrap: input.bootstrap
|
|
132
155
|
? {
|
|
133
|
-
session_id: String(input.bootstrap.session_id ??
|
|
134
|
-
session_epoch:
|
|
135
|
-
session_token: String(input.bootstrap.session_token ??
|
|
136
|
-
bundle_seq:
|
|
137
|
-
task_version:
|
|
156
|
+
session_id: String(input.bootstrap.session_id ?? '').trim(),
|
|
157
|
+
session_epoch: finiteNumber(input.bootstrap.session_epoch, 0),
|
|
158
|
+
session_token: String(input.bootstrap.session_token ?? ''),
|
|
159
|
+
bundle_seq: finiteNumber(input.bootstrap.bundle_seq, 1),
|
|
160
|
+
task_version: finiteNumber(input.bootstrap.task_version, 1),
|
|
138
161
|
}
|
|
139
162
|
: null,
|
|
140
163
|
};
|
|
141
164
|
}
|
|
142
165
|
function sanitize(value) {
|
|
143
|
-
return value.replace(/[^a-zA-Z0-9_-]+/g,
|
|
166
|
+
return value.replace(/[^a-zA-Z0-9_-]+/g, '-').replace(/^-+|-+$/g, '').toLowerCase();
|
|
144
167
|
}
|
|
145
168
|
function firstExistingPath(paths) {
|
|
146
169
|
for (const filePath of paths) {
|
|
@@ -155,7 +178,7 @@ function parseLooseJson(raw) {
|
|
|
155
178
|
return JSON.parse(raw);
|
|
156
179
|
}
|
|
157
180
|
catch {
|
|
158
|
-
for (let index = raw.lastIndexOf(
|
|
181
|
+
for (let index = raw.lastIndexOf('}'); index >= 0; index = raw.lastIndexOf('}', index - 1)) {
|
|
159
182
|
const trimmed = raw.slice(0, index + 1).trim();
|
|
160
183
|
try {
|
|
161
184
|
return JSON.parse(trimmed);
|
|
@@ -164,25 +187,36 @@ function parseLooseJson(raw) {
|
|
|
164
187
|
// keep walking backward until we hit the real JSON terminator
|
|
165
188
|
}
|
|
166
189
|
}
|
|
167
|
-
throw new SyntaxError(
|
|
190
|
+
throw new SyntaxError('Unable to parse host config JSON');
|
|
168
191
|
}
|
|
169
192
|
}
|
|
170
|
-
async function
|
|
171
|
-
const tempPath = `${filePath}.tmp`;
|
|
193
|
+
async function quarantineCorruptFile(filePath, raw, error) {
|
|
172
194
|
try {
|
|
173
|
-
await
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
if (
|
|
180
|
-
|
|
181
|
-
|
|
195
|
+
await mkdir(getHostDiagnosticsDir(), { recursive: true });
|
|
196
|
+
const stamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
197
|
+
const target = path.join(getHostDiagnosticsDir(), `${path.basename(filePath)}.corrupt-${stamp}.json`);
|
|
198
|
+
if (raw) {
|
|
199
|
+
await writeFile(target, raw, 'utf8');
|
|
200
|
+
}
|
|
201
|
+
else if (existsSync(filePath)) {
|
|
202
|
+
await rename(filePath, target);
|
|
203
|
+
return;
|
|
182
204
|
}
|
|
183
|
-
|
|
205
|
+
await appendHostLog(`[config] quarantined corrupt config from ${filePath}: ${error instanceof Error ? error.message : String(error)}`);
|
|
184
206
|
}
|
|
207
|
+
catch {
|
|
208
|
+
// ignore quarantine failures; returning null is still safer than crashing the CLI
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
async function writeAtomicJson(filePath, value) {
|
|
212
|
+
const tempPath = `${filePath}.tmp`;
|
|
213
|
+
await writeFile(tempPath, JSON.stringify(value, null, 2), 'utf8');
|
|
214
|
+
await rename(tempPath, filePath);
|
|
215
|
+
}
|
|
216
|
+
function finiteNumber(value, fallback) {
|
|
217
|
+
const parsed = Number(value);
|
|
218
|
+
return Number.isFinite(parsed) ? parsed : fallback;
|
|
185
219
|
}
|
|
186
220
|
function baseAppDataDir() {
|
|
187
|
-
return process.env.APPDATA ?? path.join(process.env.USERPROFILE ?? os.homedir(),
|
|
221
|
+
return process.env.APPDATA ?? path.join(process.env.USERPROFILE ?? os.homedir(), 'AppData', 'Roaming');
|
|
188
222
|
}
|
|
@@ -38,11 +38,21 @@ export function probeRunnerAvailability(kind, override) {
|
|
|
38
38
|
if (process.platform === "win32") {
|
|
39
39
|
return { available: existsSync(command) || queryWhere(command).length > 0, command };
|
|
40
40
|
}
|
|
41
|
-
|
|
41
|
+
const result = spawnSync('sh', ['-lc', `command -v ${shellEscape(command)} >/dev/null 2>&1`], {
|
|
42
|
+
windowsHide: true,
|
|
43
|
+
});
|
|
44
|
+
return { available: result.status === 0, command };
|
|
45
|
+
}
|
|
46
|
+
export function assertRunnerAvailable(kind, override) {
|
|
47
|
+
const probe = probeRunnerAvailability(kind, override);
|
|
48
|
+
if (!probe.available) {
|
|
49
|
+
throw new Error(`${kind} is not installed or is not on PATH.`);
|
|
50
|
+
}
|
|
51
|
+
return probe.command;
|
|
42
52
|
}
|
|
43
53
|
async function runCodex(input) {
|
|
44
54
|
const outputDir = await mkdtemp(path.join(os.tmpdir(), "office-codex-"));
|
|
45
|
-
const command =
|
|
55
|
+
const command = assertRunnerAvailable("codex", process.env.CODEX_CMD);
|
|
46
56
|
const args = [
|
|
47
57
|
"exec",
|
|
48
58
|
"--json",
|
|
@@ -64,7 +74,7 @@ async function runCodex(input) {
|
|
|
64
74
|
}
|
|
65
75
|
}
|
|
66
76
|
async function runClaude(input) {
|
|
67
|
-
const command =
|
|
77
|
+
const command = assertRunnerAvailable("claude", process.env.CLAUDE_CMD);
|
|
68
78
|
const args = [
|
|
69
79
|
"-p",
|
|
70
80
|
"--verbose",
|
|
@@ -94,6 +104,9 @@ function queryWhere(command) {
|
|
|
94
104
|
.map((line) => line.trim())
|
|
95
105
|
.filter(Boolean);
|
|
96
106
|
}
|
|
107
|
+
function shellEscape(value) {
|
|
108
|
+
return `'${String(value).replace(/'/g, `'\''`)}'`;
|
|
109
|
+
}
|
|
97
110
|
function buildWindowsFallbacks(kind) {
|
|
98
111
|
const localAppData = process.env.LOCALAPPDATA ?? "";
|
|
99
112
|
const appData = process.env.APPDATA ?? "";
|