codelark 0.1.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/LICENSE +21 -0
- package/README.md +193 -0
- package/SECURITY.md +34 -0
- package/SKILL.md +67 -0
- package/agents/openai.yaml +4 -0
- package/dist/cli.mjs +8794 -0
- package/dist/daemon.mjs +47172 -0
- package/dist/ui-server.mjs +22165 -0
- package/package.json +73 -0
- package/schemas/config.v1.schema.json +259 -0
- package/schemas/data/audit.v1.schema.json +44 -0
- package/schemas/data/auto-tasks.v1.schema.json +94 -0
- package/schemas/data/channel-chats.v1.schema.json +159 -0
- package/schemas/data/channel-default-targets.v1.schema.json +43 -0
- package/schemas/data/messages.v1.schema.json +23 -0
- package/schemas/data/number-map.v1.schema.json +9 -0
- package/schemas/data/permissions.v1.schema.json +35 -0
- package/schemas/data/sessions.v1.schema.json +330 -0
- package/schemas/data/string-map.v1.schema.json +9 -0
- package/schemas/manifest.json +121 -0
- package/scripts/analyze-bridge-log.js +838 -0
- package/scripts/build-preflight.d.ts +21 -0
- package/scripts/build-preflight.js +70 -0
- package/scripts/build.js +53 -0
- package/scripts/check-npm-pack.js +46 -0
- package/scripts/daemon.ps1 +16 -0
- package/scripts/daemon.sh +206 -0
- package/scripts/doctor.ps1 +27 -0
- package/scripts/doctor.sh +185 -0
- package/scripts/hot-update-bridge.sh +298 -0
- package/scripts/install-codex-skills.sh +127 -0
- package/scripts/install-codex.sh +10 -0
- package/scripts/migrate-bindings-to-channel-chats.js +228 -0
- package/scripts/patch-codex-sdk-windows-hide.js +96 -0
- package/scripts/real-feishu-e2e.ts +5804 -0
- package/scripts/run-tests.js +83 -0
- package/scripts/setup-wizard-real-e2e.ts +195 -0
- package/scripts/supervisor-linux.sh +49 -0
- package/scripts/supervisor-macos.sh +167 -0
- package/scripts/supervisor-windows.ps1 +481 -0
- package/skills/codelark/SKILL.md +67 -0
- package/skills/codelark-auto/SKILL.md +80 -0
- package/skills/codelark-question/SKILL.md +54 -0
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import os from 'node:os';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { spawn } from 'node:child_process';
|
|
5
|
+
|
|
6
|
+
const tempHome = fs.mkdtempSync(path.join(os.tmpdir(), 'codelark-test-'));
|
|
7
|
+
const runtimeHome = path.join(tempHome, 'runtime-home');
|
|
8
|
+
const codexHome = path.join(tempHome, 'codex-home');
|
|
9
|
+
const claudeHome = path.join(tempHome, 'claude-home');
|
|
10
|
+
fs.mkdirSync(runtimeHome, { recursive: true });
|
|
11
|
+
fs.mkdirSync(codexHome, { recursive: true });
|
|
12
|
+
fs.mkdirSync(claudeHome, { recursive: true });
|
|
13
|
+
|
|
14
|
+
const testsDir = path.join(process.cwd(), 'src', '__tests__');
|
|
15
|
+
|
|
16
|
+
function discoverTestFiles(dir) {
|
|
17
|
+
return fs.readdirSync(dir, { withFileTypes: true }).flatMap((entry) => {
|
|
18
|
+
const fullPath = path.join(dir, entry.name);
|
|
19
|
+
if (entry.isDirectory()) {
|
|
20
|
+
return discoverTestFiles(fullPath);
|
|
21
|
+
}
|
|
22
|
+
if (!entry.isFile() || !entry.name.endsWith('.test.ts')) {
|
|
23
|
+
return [];
|
|
24
|
+
}
|
|
25
|
+
return [path.relative(process.cwd(), fullPath)];
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const layerFilters = new Map([
|
|
30
|
+
['--unit', path.join('src', '__tests__', 'unit') + path.sep],
|
|
31
|
+
['--workflow', path.join('src', '__tests__', 'workflow') + path.sep],
|
|
32
|
+
['--mock-e2e', path.join('src', '__tests__', 'e2e', 'mock-app') + path.sep],
|
|
33
|
+
['--local-e2e', path.join('src', '__tests__', 'e2e', 'local-process') + path.sep],
|
|
34
|
+
['--harness', path.join('src', '__tests__', 'harness') + path.sep],
|
|
35
|
+
]);
|
|
36
|
+
|
|
37
|
+
const requestedLayers = process.argv.slice(2).filter((arg) => layerFilters.has(arg));
|
|
38
|
+
const testFiles = discoverTestFiles(testsDir)
|
|
39
|
+
.sort()
|
|
40
|
+
.filter((file) => requestedLayers.length === 0 || requestedLayers.some((arg) => file.startsWith(layerFilters.get(arg))));
|
|
41
|
+
|
|
42
|
+
if (testFiles.length === 0) {
|
|
43
|
+
console.error(`No test files matched ${requestedLayers.join(', ') || 'the current test discovery pattern'}.`);
|
|
44
|
+
process.exit(1);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const child = spawn(
|
|
48
|
+
process.execPath,
|
|
49
|
+
[
|
|
50
|
+
'--test',
|
|
51
|
+
'--test-concurrency=1',
|
|
52
|
+
'--import',
|
|
53
|
+
'tsx',
|
|
54
|
+
'--test-timeout=15000',
|
|
55
|
+
...testFiles,
|
|
56
|
+
],
|
|
57
|
+
{
|
|
58
|
+
stdio: 'inherit',
|
|
59
|
+
env: {
|
|
60
|
+
...process.env,
|
|
61
|
+
HOME: runtimeHome,
|
|
62
|
+
USERPROFILE: runtimeHome,
|
|
63
|
+
CODELARK_HOME: tempHome,
|
|
64
|
+
CODEX_HOME: codexHome,
|
|
65
|
+
CODELARK_CLAUDE_HOME: claudeHome,
|
|
66
|
+
},
|
|
67
|
+
},
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
child.on('exit', (code, signal) => {
|
|
71
|
+
try {
|
|
72
|
+
fs.rmSync(tempHome, { recursive: true, force: true });
|
|
73
|
+
} catch {
|
|
74
|
+
// ignore
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (signal) {
|
|
78
|
+
process.kill(process.pid, signal);
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
process.exit(code ?? 1);
|
|
83
|
+
});
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import os from 'node:os';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
|
|
6
|
+
type FeishuSite = 'feishu' | 'lark';
|
|
7
|
+
|
|
8
|
+
function valueArg(args: string[], name: string, fallback = ''): string {
|
|
9
|
+
const index = args.indexOf(name);
|
|
10
|
+
if (index < 0) return fallback;
|
|
11
|
+
return args[index + 1] || fallback;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function hasFlag(args: string[], name: string): boolean {
|
|
15
|
+
return args.includes(name);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function printUsage(): void {
|
|
19
|
+
process.stdout.write([
|
|
20
|
+
'Usage:',
|
|
21
|
+
' CODELARK_SETUP_WIZARD_REAL_E2E=1 npm run real:setup-wizard:e2e -- [options]',
|
|
22
|
+
'',
|
|
23
|
+
'Options:',
|
|
24
|
+
' --run-root <path> Temporary root; default /tmp/clk-setup-wizard-real-e2e-<timestamp>',
|
|
25
|
+
' --test-env-file <path> Load test app credentials from an env file; avoids npm argument echo',
|
|
26
|
+
' --app-id <cli_...> App ID written to isolated lark-cli/CodeLark config',
|
|
27
|
+
' --app-secret <secret> App Secret for the isolated smoke app; prefer env file/env vars to avoid npm echo',
|
|
28
|
+
' --site <feishu|lark> Site brand; default feishu',
|
|
29
|
+
' --keep-temp Keep temporary root for diagnosis; default cleans it in success and failure paths',
|
|
30
|
+
' --simulate-failure-after-sync Test cleanup on a post-lark-cli failure',
|
|
31
|
+
' --help Show this help',
|
|
32
|
+
'',
|
|
33
|
+
].join('\n'));
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function parseEnvFile(filePath: string): Record<string, string> {
|
|
37
|
+
if (!filePath) return {};
|
|
38
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
39
|
+
const values: Record<string, string> = {};
|
|
40
|
+
for (const rawLine of content.split(/\r?\n/)) {
|
|
41
|
+
const line = rawLine.trim();
|
|
42
|
+
if (!line || line.startsWith('#')) continue;
|
|
43
|
+
const separator = line.indexOf('=');
|
|
44
|
+
if (separator <= 0) continue;
|
|
45
|
+
const key = line.slice(0, separator).trim();
|
|
46
|
+
let value = line.slice(separator + 1).trim();
|
|
47
|
+
if (
|
|
48
|
+
(value.startsWith('"') && value.endsWith('"'))
|
|
49
|
+
|| (value.startsWith("'") && value.endsWith("'"))
|
|
50
|
+
) {
|
|
51
|
+
value = value.slice(1, -1);
|
|
52
|
+
}
|
|
53
|
+
values[key] = value;
|
|
54
|
+
}
|
|
55
|
+
return values;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function envValue(envFile: Record<string, string>, key: string, legacyKey?: string): string {
|
|
59
|
+
return process.env[key] || envFile[key] || (legacyKey ? process.env[legacyKey] || envFile[legacyKey] || '' : '');
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function assertInside(parentPath: string, childPath: string): void {
|
|
63
|
+
const parent = path.resolve(parentPath);
|
|
64
|
+
const child = path.resolve(childPath);
|
|
65
|
+
const relative = path.relative(parent, child);
|
|
66
|
+
if (relative === '' || (relative && !relative.startsWith('..') && !path.isAbsolute(relative))) return;
|
|
67
|
+
throw new Error(`Path escaped temp root: ${child}`);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
async function main(): Promise<void> {
|
|
71
|
+
const argv = process.argv.slice(2);
|
|
72
|
+
if (hasFlag(argv, '--help')) {
|
|
73
|
+
printUsage();
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
if (process.env.CODELARK_SETUP_WIZARD_REAL_E2E !== '1') {
|
|
77
|
+
throw new Error('Refusing to run setup wizard real e2e without CODELARK_SETUP_WIZARD_REAL_E2E=1.');
|
|
78
|
+
}
|
|
79
|
+
const testEnvFile = valueArg(argv, '--test-env-file', '');
|
|
80
|
+
const envFile = parseEnvFile(testEnvFile);
|
|
81
|
+
|
|
82
|
+
const runRoot = path.resolve(valueArg(
|
|
83
|
+
argv,
|
|
84
|
+
'--run-root',
|
|
85
|
+
path.join(os.tmpdir(), `clk-setup-wizard-real-e2e-${Date.now()}`),
|
|
86
|
+
));
|
|
87
|
+
const keepTemp = hasFlag(argv, '--keep-temp');
|
|
88
|
+
const appId = valueArg(
|
|
89
|
+
argv,
|
|
90
|
+
'--app-id',
|
|
91
|
+
envValue(envFile, 'CODELARK_REAL_FEISHU_TEST_APP_ID', 'CTI_REAL_FEISHU_TEST_APP_ID') || 'cli_setup_wizard_real_e2e',
|
|
92
|
+
);
|
|
93
|
+
const appSecret = valueArg(
|
|
94
|
+
argv,
|
|
95
|
+
'--app-secret',
|
|
96
|
+
envValue(envFile, 'CODELARK_REAL_FEISHU_TEST_APP_SECRET', 'CTI_REAL_FEISHU_TEST_APP_SECRET') || 'setup-wizard-real-e2e-secret',
|
|
97
|
+
);
|
|
98
|
+
const siteArg = valueArg(
|
|
99
|
+
argv,
|
|
100
|
+
'--site',
|
|
101
|
+
envValue(envFile, 'CODELARK_REAL_FEISHU_TEST_SITE', 'CTI_REAL_FEISHU_TEST_SITE') || 'feishu',
|
|
102
|
+
);
|
|
103
|
+
const site: FeishuSite = siteArg === 'lark' ? 'lark' : 'feishu';
|
|
104
|
+
|
|
105
|
+
const runtimeHome = path.join(runRoot, 'home');
|
|
106
|
+
const codelarkHome = path.join(runRoot, 'clk-home');
|
|
107
|
+
const workspaceRoot = path.join(runRoot, 'workspace');
|
|
108
|
+
const larkConfigPath = path.join(runtimeHome, '.lark-cli', 'config.json');
|
|
109
|
+
const configEnvPath = path.join(codelarkHome, 'config.env');
|
|
110
|
+
const configJsonPath = path.join(codelarkHome, 'config.json');
|
|
111
|
+
|
|
112
|
+
try {
|
|
113
|
+
assertInside(os.tmpdir(), runRoot);
|
|
114
|
+
fs.mkdirSync(runtimeHome, { recursive: true });
|
|
115
|
+
fs.mkdirSync(codelarkHome, { recursive: true });
|
|
116
|
+
fs.mkdirSync(workspaceRoot, { recursive: true });
|
|
117
|
+
fs.writeFileSync(configEnvPath, '# custom env survives setup\nCUSTOM_KEEP=1\n', { mode: 0o600 });
|
|
118
|
+
|
|
119
|
+
process.env.HOME = runtimeHome;
|
|
120
|
+
process.env.USERPROFILE = runtimeHome;
|
|
121
|
+
process.env.CODELARK_HOME = codelarkHome;
|
|
122
|
+
|
|
123
|
+
const configuration = await import('../src/configuration/index.js');
|
|
124
|
+
const setupWizard = await import('../src/entrypoints/setup-wizard.js');
|
|
125
|
+
|
|
126
|
+
const credentials = {
|
|
127
|
+
appId,
|
|
128
|
+
appSecret,
|
|
129
|
+
site,
|
|
130
|
+
alias: 'codelark',
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
await setupWizard.syncLarkCliCredentials(credentials, runtimeHome);
|
|
134
|
+
const entriesAfterSync = setupWizard.readLarkCliAppEntries(larkConfigPath);
|
|
135
|
+
const syncedEntry = entriesAfterSync.find((entry) => entry.appId === appId);
|
|
136
|
+
if (!syncedEntry) throw new Error(`lark-cli config missing synced app ${appId}`);
|
|
137
|
+
if (syncedEntry.brand !== site) throw new Error(`lark-cli brand mismatch: ${syncedEntry.brand}`);
|
|
138
|
+
if (syncedEntry.appSecret) {
|
|
139
|
+
throw new Error('real lark-cli e2e expected lark-cli to avoid plaintext appSecret in this environment');
|
|
140
|
+
}
|
|
141
|
+
if (syncedEntry.secretStorage !== 'keychain') {
|
|
142
|
+
throw new Error(`expected keychain/local encrypted lark-cli secret storage, got ${syncedEntry.secretStorage}`);
|
|
143
|
+
}
|
|
144
|
+
const importableApps = setupWizard.readLarkCliApps(larkConfigPath);
|
|
145
|
+
const importableApp = importableApps.find((entry) => entry.appId === appId);
|
|
146
|
+
if (!importableApp) throw new Error(`CodeLark could not import synced lark-cli app ${appId}`);
|
|
147
|
+
if (importableApp.appSecret !== appSecret) {
|
|
148
|
+
throw new Error('CodeLark did not recover App Secret from lark-cli local encrypted storage');
|
|
149
|
+
}
|
|
150
|
+
if (hasFlag(argv, '--simulate-failure-after-sync')) {
|
|
151
|
+
throw new Error('simulated setup wizard real e2e failure after lark-cli sync');
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const current = configuration.loadConfig();
|
|
155
|
+
configuration.saveConfig(setupWizard.buildSetupConfig(current, credentials, 'codex', workspaceRoot));
|
|
156
|
+
|
|
157
|
+
const savedConfig = configuration.loadConfig();
|
|
158
|
+
const savedFeishu = savedConfig.channels?.find((channel) => channel.provider === 'feishu');
|
|
159
|
+
if (savedConfig.runtime !== 'codex') throw new Error(`runtime mismatch: ${savedConfig.runtime}`);
|
|
160
|
+
if (savedConfig.defaultWorkspaceRoot !== workspaceRoot) {
|
|
161
|
+
throw new Error(`workspace mismatch: ${savedConfig.defaultWorkspaceRoot}`);
|
|
162
|
+
}
|
|
163
|
+
if (savedFeishu?.config.appId !== appId) throw new Error('config.v1 appId mismatch');
|
|
164
|
+
if (savedFeishu?.config.appSecret !== appSecret) throw new Error('config.v1 appSecret mismatch');
|
|
165
|
+
if (savedFeishu?.config.site !== site) throw new Error('config.v1 site mismatch');
|
|
166
|
+
|
|
167
|
+
const envContent = fs.readFileSync(configEnvPath, 'utf-8');
|
|
168
|
+
if (!envContent.includes('CUSTOM_KEEP=1')) throw new Error('custom config.env line was not preserved');
|
|
169
|
+
if (!envContent.includes(`CODELARK_FEISHU_APP_ID=${appId}`)) throw new Error('config.env appId missing');
|
|
170
|
+
if (!envContent.includes(`CODELARK_FEISHU_APP_SECRET=${appSecret}`)) throw new Error('config.env appSecret missing');
|
|
171
|
+
if (!fs.existsSync(configJsonPath)) throw new Error('config.json missing');
|
|
172
|
+
|
|
173
|
+
const result = {
|
|
174
|
+
ok: true,
|
|
175
|
+
runRoot,
|
|
176
|
+
runtimeHome,
|
|
177
|
+
codelarkHome,
|
|
178
|
+
larkConfigPath,
|
|
179
|
+
configEnvPath,
|
|
180
|
+
configJsonPath,
|
|
181
|
+
larkSecretStorage: syncedEntry.secretStorage,
|
|
182
|
+
cleanedRunRoot: !keepTemp,
|
|
183
|
+
};
|
|
184
|
+
process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
|
|
185
|
+
} finally {
|
|
186
|
+
if (!keepTemp) {
|
|
187
|
+
fs.rmSync(runRoot, { recursive: true, force: true });
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
main().catch((error) => {
|
|
193
|
+
process.stderr.write(`${error instanceof Error ? error.stack || error.message : String(error)}\n`);
|
|
194
|
+
process.exit(1);
|
|
195
|
+
});
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Linux supervisor — setsid/nohup fallback process management.
|
|
3
|
+
# Sourced by daemon.sh; expects CODELARK_HOME, SKILL_DIR, PID_FILE, STATUS_FILE, LOG_FILE.
|
|
4
|
+
|
|
5
|
+
# ── Public interface (called by daemon.sh) ──
|
|
6
|
+
|
|
7
|
+
supervisor_start() {
|
|
8
|
+
if command -v setsid >/dev/null 2>&1; then
|
|
9
|
+
setsid node "$SKILL_DIR/dist/daemon.mjs" >> "$LOG_FILE" 2>&1 < /dev/null &
|
|
10
|
+
else
|
|
11
|
+
nohup node "$SKILL_DIR/dist/daemon.mjs" >> "$LOG_FILE" 2>&1 < /dev/null &
|
|
12
|
+
fi
|
|
13
|
+
# Fallback: write shell $! as PID; main.ts will overwrite with real PID
|
|
14
|
+
echo $! > "$PID_FILE"
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
supervisor_stop() {
|
|
18
|
+
local pid
|
|
19
|
+
pid=$(read_pid)
|
|
20
|
+
if [ -z "$pid" ]; then echo "No bridge running"; return 0; fi
|
|
21
|
+
if pid_alive "$pid"; then
|
|
22
|
+
kill "$pid"
|
|
23
|
+
for _ in $(seq 1 10); do
|
|
24
|
+
pid_alive "$pid" || break
|
|
25
|
+
sleep 1
|
|
26
|
+
done
|
|
27
|
+
pid_alive "$pid" && kill -9 "$pid"
|
|
28
|
+
echo "Bridge stopped"
|
|
29
|
+
else
|
|
30
|
+
echo "Bridge was not running (stale PID file)"
|
|
31
|
+
fi
|
|
32
|
+
rm -f "$PID_FILE"
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
supervisor_is_managed() {
|
|
36
|
+
# Linux fallback has no service manager; always false
|
|
37
|
+
return 1
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
supervisor_status_extra() {
|
|
41
|
+
# No extra status for Linux fallback
|
|
42
|
+
:
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
supervisor_is_running() {
|
|
46
|
+
local pid
|
|
47
|
+
pid=$(read_pid)
|
|
48
|
+
pid_alive "$pid"
|
|
49
|
+
}
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# macOS supervisor — launchd-based process management.
|
|
3
|
+
# Sourced by daemon.sh; expects CODELARK_HOME, SKILL_DIR, PID_FILE, STATUS_FILE, LOG_FILE.
|
|
4
|
+
|
|
5
|
+
LAUNCHD_LABEL="com.codelark.bridge"
|
|
6
|
+
PLIST_DIR="$HOME/Library/LaunchAgents"
|
|
7
|
+
PLIST_FILE="$PLIST_DIR/$LAUNCHD_LABEL.plist"
|
|
8
|
+
|
|
9
|
+
# ── launchd helpers ──
|
|
10
|
+
|
|
11
|
+
launchd_target() {
|
|
12
|
+
local label="$1"
|
|
13
|
+
echo "gui/$(id -u)/$label"
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
launchd_bootout() {
|
|
17
|
+
local label="$1"
|
|
18
|
+
launchctl bootout "$(launchd_target "$label")" 2>/dev/null || true
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
launchd_print() {
|
|
22
|
+
local label="$1"
|
|
23
|
+
launchctl print "$(launchd_target "$label")" 2>/dev/null
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
launchd_active_label() {
|
|
27
|
+
if launchd_print "$LAUNCHD_LABEL" >/dev/null; then
|
|
28
|
+
echo "$LAUNCHD_LABEL"
|
|
29
|
+
return 0
|
|
30
|
+
fi
|
|
31
|
+
return 1
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
launchd_pid_for_label() {
|
|
35
|
+
local label="$1"
|
|
36
|
+
launchd_print "$label" | grep -m1 'pid = ' | sed 's/.*pid = //' | tr -d ' '
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
# Collect env vars that should be forwarded into the plist.
|
|
40
|
+
# We honour clean_env() logic by reading *after* clean_env runs.
|
|
41
|
+
build_env_dict() {
|
|
42
|
+
local indent=" "
|
|
43
|
+
local dict=""
|
|
44
|
+
|
|
45
|
+
# Always forward basics
|
|
46
|
+
for var in HOME PATH USER SHELL LANG TMPDIR; do
|
|
47
|
+
local val="${!var:-}"
|
|
48
|
+
[ -z "$val" ] && continue
|
|
49
|
+
dict+="${indent}<key>${var}</key>\n${indent}<string>${val}</string>\n"
|
|
50
|
+
done
|
|
51
|
+
|
|
52
|
+
# Forward CodeLark-specific vars.
|
|
53
|
+
while IFS='=' read -r name val; do
|
|
54
|
+
case "$name" in CODELARK_*)
|
|
55
|
+
dict+="${indent}<key>${name}</key>\n${indent}<string>${val}</string>\n"
|
|
56
|
+
;; esac
|
|
57
|
+
done < <(env)
|
|
58
|
+
|
|
59
|
+
# Forward Codex/OpenAI credentials used by the Codex runtime.
|
|
60
|
+
for var in OPENAI_API_KEY CODEX_API_KEY CODELARK_CODEX_API_KEY CODELARK_CODEX_BASE_URL; do
|
|
61
|
+
local val="${!var:-}"
|
|
62
|
+
[ -z "$val" ] && continue
|
|
63
|
+
dict+="${indent}<key>${var}</key>\n${indent}<string>${val}</string>\n"
|
|
64
|
+
done
|
|
65
|
+
|
|
66
|
+
echo -e "$dict"
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
generate_plist() {
|
|
70
|
+
local node_path
|
|
71
|
+
node_path=$(command -v node)
|
|
72
|
+
|
|
73
|
+
mkdir -p "$PLIST_DIR"
|
|
74
|
+
local env_dict
|
|
75
|
+
env_dict=$(build_env_dict)
|
|
76
|
+
|
|
77
|
+
cat > "$PLIST_FILE" <<PLIST
|
|
78
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
79
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
80
|
+
<plist version="1.0">
|
|
81
|
+
<dict>
|
|
82
|
+
<key>Label</key>
|
|
83
|
+
<string>${LAUNCHD_LABEL}</string>
|
|
84
|
+
|
|
85
|
+
<key>ProgramArguments</key>
|
|
86
|
+
<array>
|
|
87
|
+
<string>${node_path}</string>
|
|
88
|
+
<string>${SKILL_DIR}/dist/daemon.mjs</string>
|
|
89
|
+
</array>
|
|
90
|
+
|
|
91
|
+
<key>WorkingDirectory</key>
|
|
92
|
+
<string>${SKILL_DIR}</string>
|
|
93
|
+
|
|
94
|
+
<key>StandardOutPath</key>
|
|
95
|
+
<string>${LOG_FILE}</string>
|
|
96
|
+
<key>StandardErrorPath</key>
|
|
97
|
+
<string>${LOG_FILE}</string>
|
|
98
|
+
|
|
99
|
+
<key>RunAtLoad</key>
|
|
100
|
+
<false/>
|
|
101
|
+
|
|
102
|
+
<key>KeepAlive</key>
|
|
103
|
+
<dict>
|
|
104
|
+
<key>SuccessfulExit</key>
|
|
105
|
+
<false/>
|
|
106
|
+
</dict>
|
|
107
|
+
|
|
108
|
+
<key>ThrottleInterval</key>
|
|
109
|
+
<integer>10</integer>
|
|
110
|
+
|
|
111
|
+
<key>EnvironmentVariables</key>
|
|
112
|
+
<dict>
|
|
113
|
+
${env_dict} </dict>
|
|
114
|
+
</dict>
|
|
115
|
+
</plist>
|
|
116
|
+
PLIST
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
# ── Public interface (called by daemon.sh) ──
|
|
120
|
+
|
|
121
|
+
supervisor_start() {
|
|
122
|
+
launchd_bootout "$LAUNCHD_LABEL"
|
|
123
|
+
generate_plist
|
|
124
|
+
launchctl bootstrap "gui/$(id -u)" "$PLIST_FILE"
|
|
125
|
+
launchctl kickstart -k "$(launchd_target "$LAUNCHD_LABEL")"
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
supervisor_stop() {
|
|
129
|
+
launchd_bootout "$LAUNCHD_LABEL"
|
|
130
|
+
rm -f "$PID_FILE"
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
supervisor_is_managed() {
|
|
134
|
+
launchd_active_label >/dev/null
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
supervisor_status_extra() {
|
|
138
|
+
local active_label
|
|
139
|
+
active_label=$(launchd_active_label 2>/dev/null || true)
|
|
140
|
+
if [ -n "$active_label" ]; then
|
|
141
|
+
echo "Bridge is registered with launchd ($active_label)"
|
|
142
|
+
# Extract PID from launchctl as the authoritative source
|
|
143
|
+
local lc_pid
|
|
144
|
+
lc_pid=$(launchd_pid_for_label "$active_label")
|
|
145
|
+
if [ -n "$lc_pid" ] && [ "$lc_pid" != "0" ] && [ "$lc_pid" != "-" ]; then
|
|
146
|
+
echo "launchd reports PID: $lc_pid"
|
|
147
|
+
fi
|
|
148
|
+
fi
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
# Override: on macOS, check launchctl first, then fall back to PID file
|
|
152
|
+
supervisor_is_running() {
|
|
153
|
+
# Primary: launchctl knows the process
|
|
154
|
+
local active_label
|
|
155
|
+
active_label=$(launchd_active_label 2>/dev/null || true)
|
|
156
|
+
if [ -n "$active_label" ]; then
|
|
157
|
+
local lc_pid
|
|
158
|
+
lc_pid=$(launchd_pid_for_label "$active_label")
|
|
159
|
+
if [ -n "$lc_pid" ] && [ "$lc_pid" != "0" ] && [ "$lc_pid" != "-" ]; then
|
|
160
|
+
return 0
|
|
161
|
+
fi
|
|
162
|
+
fi
|
|
163
|
+
# Fallback: PID file
|
|
164
|
+
local pid
|
|
165
|
+
pid=$(read_pid)
|
|
166
|
+
pid_alive "$pid"
|
|
167
|
+
}
|