uxnan-bridge 0.0.1-alpha.20260621
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/README.md +150 -0
- package/dist/src/account-status.d.ts +13 -0
- package/dist/src/account-status.js +78 -0
- package/dist/src/account-status.js.map +1 -0
- package/dist/src/adapters/base-adapter.d.ts +18 -0
- package/dist/src/adapters/base-adapter.js +15 -0
- package/dist/src/adapters/base-adapter.js.map +1 -0
- package/dist/src/adapters/claude-adapter.d.ts +102 -0
- package/dist/src/adapters/claude-adapter.js +486 -0
- package/dist/src/adapters/claude-adapter.js.map +1 -0
- package/dist/src/adapters/claude-tools.d.ts +25 -0
- package/dist/src/adapters/claude-tools.js +146 -0
- package/dist/src/adapters/claude-tools.js.map +1 -0
- package/dist/src/adapters/codex-adapter.d.ts +116 -0
- package/dist/src/adapters/codex-adapter.js +912 -0
- package/dist/src/adapters/codex-adapter.js.map +1 -0
- package/dist/src/adapters/codex-app-server.d.ts +74 -0
- package/dist/src/adapters/codex-app-server.js +225 -0
- package/dist/src/adapters/codex-app-server.js.map +1 -0
- package/dist/src/adapters/codex-approval.d.ts +88 -0
- package/dist/src/adapters/codex-approval.js +160 -0
- package/dist/src/adapters/codex-approval.js.map +1 -0
- package/dist/src/adapters/codex-tools.d.ts +18 -0
- package/dist/src/adapters/codex-tools.js +106 -0
- package/dist/src/adapters/codex-tools.js.map +1 -0
- package/dist/src/adapters/content-blocks.d.ts +68 -0
- package/dist/src/adapters/content-blocks.js +205 -0
- package/dist/src/adapters/content-blocks.js.map +1 -0
- package/dist/src/adapters/echo-agent-adapter.d.ts +23 -0
- package/dist/src/adapters/echo-agent-adapter.js +72 -0
- package/dist/src/adapters/echo-agent-adapter.js.map +1 -0
- package/dist/src/adapters/gemini-adapter.d.ts +87 -0
- package/dist/src/adapters/gemini-adapter.js +594 -0
- package/dist/src/adapters/gemini-adapter.js.map +1 -0
- package/dist/src/adapters/gemini-tools.d.ts +4 -0
- package/dist/src/adapters/gemini-tools.js +48 -0
- package/dist/src/adapters/gemini-tools.js.map +1 -0
- package/dist/src/adapters/opencode-adapter.d.ts +74 -0
- package/dist/src/adapters/opencode-adapter.js +418 -0
- package/dist/src/adapters/opencode-adapter.js.map +1 -0
- package/dist/src/adapters/opencode-tools.d.ts +2 -0
- package/dist/src/adapters/opencode-tools.js +41 -0
- package/dist/src/adapters/opencode-tools.js.map +1 -0
- package/dist/src/adapters/pi-adapter.d.ts +92 -0
- package/dist/src/adapters/pi-adapter.js +467 -0
- package/dist/src/adapters/pi-adapter.js.map +1 -0
- package/dist/src/adapters/pi-tools.d.ts +10 -0
- package/dist/src/adapters/pi-tools.js +72 -0
- package/dist/src/adapters/pi-tools.js.map +1 -0
- package/dist/src/adapters/process-agent-adapter.d.ts +24 -0
- package/dist/src/adapters/process-agent-adapter.js +111 -0
- package/dist/src/adapters/process-agent-adapter.js.map +1 -0
- package/dist/src/adapters/resolve-claude.d.ts +13 -0
- package/dist/src/adapters/resolve-claude.js +57 -0
- package/dist/src/adapters/resolve-claude.js.map +1 -0
- package/dist/src/adapters/resolve-codex.d.ts +13 -0
- package/dist/src/adapters/resolve-codex.js +48 -0
- package/dist/src/adapters/resolve-codex.js.map +1 -0
- package/dist/src/adapters/resolve-gemini.d.ts +13 -0
- package/dist/src/adapters/resolve-gemini.js +47 -0
- package/dist/src/adapters/resolve-gemini.js.map +1 -0
- package/dist/src/adapters/resolve-opencode.d.ts +11 -0
- package/dist/src/adapters/resolve-opencode.js +49 -0
- package/dist/src/adapters/resolve-opencode.js.map +1 -0
- package/dist/src/adapters/resolve-pi.d.ts +13 -0
- package/dist/src/adapters/resolve-pi.js +46 -0
- package/dist/src/adapters/resolve-pi.js.map +1 -0
- package/dist/src/adapters/run-options.d.ts +22 -0
- package/dist/src/adapters/run-options.js +48 -0
- package/dist/src/adapters/run-options.js.map +1 -0
- package/dist/src/adapters/spawn.d.ts +20 -0
- package/dist/src/adapters/spawn.js +16 -0
- package/dist/src/adapters/spawn.js.map +1 -0
- package/dist/src/agents/agent-manager.d.ts +98 -0
- package/dist/src/agents/agent-manager.js +433 -0
- package/dist/src/agents/agent-manager.js.map +1 -0
- package/dist/src/agents/attachments.d.ts +28 -0
- package/dist/src/agents/attachments.js +121 -0
- package/dist/src/agents/attachments.js.map +1 -0
- package/dist/src/bridge-context.d.ts +45 -0
- package/dist/src/bridge-context.js +2 -0
- package/dist/src/bridge-context.js.map +1 -0
- package/dist/src/bridge-status.d.ts +12 -0
- package/dist/src/bridge-status.js +17 -0
- package/dist/src/bridge-status.js.map +1 -0
- package/dist/src/bridge.d.ts +37 -0
- package/dist/src/bridge.js +446 -0
- package/dist/src/bridge.js.map +1 -0
- package/dist/src/cli.d.ts +2 -0
- package/dist/src/cli.js +194 -0
- package/dist/src/cli.js.map +1 -0
- package/dist/src/conversation/session-history.d.ts +27 -0
- package/dist/src/conversation/session-history.js +1082 -0
- package/dist/src/conversation/session-history.js.map +1 -0
- package/dist/src/conversation/thread-store.d.ts +74 -0
- package/dist/src/conversation/thread-store.js +366 -0
- package/dist/src/conversation/thread-store.js.map +1 -0
- package/dist/src/daemon-config.d.ts +123 -0
- package/dist/src/daemon-config.js +64 -0
- package/dist/src/daemon-config.js.map +1 -0
- package/dist/src/daemon-state.d.ts +27 -0
- package/dist/src/daemon-state.js +76 -0
- package/dist/src/daemon-state.js.map +1 -0
- package/dist/src/git/git-runner.d.ts +24 -0
- package/dist/src/git/git-runner.js +63 -0
- package/dist/src/git/git-runner.js.map +1 -0
- package/dist/src/git/git-service.d.ts +76 -0
- package/dist/src/git/git-service.js +435 -0
- package/dist/src/git/git-service.js.map +1 -0
- package/dist/src/handler-router.d.ts +34 -0
- package/dist/src/handler-router.js +67 -0
- package/dist/src/handler-router.js.map +1 -0
- package/dist/src/handlers/account-handler.d.ts +4 -0
- package/dist/src/handlers/account-handler.js +27 -0
- package/dist/src/handlers/account-handler.js.map +1 -0
- package/dist/src/handlers/agent-handler.d.ts +2 -0
- package/dist/src/handlers/agent-handler.js +8 -0
- package/dist/src/handlers/agent-handler.js.map +1 -0
- package/dist/src/handlers/bridge-control-handler.d.ts +2 -0
- package/dist/src/handlers/bridge-control-handler.js +64 -0
- package/dist/src/handlers/bridge-control-handler.js.map +1 -0
- package/dist/src/handlers/desktop-handler.d.ts +12 -0
- package/dist/src/handlers/desktop-handler.js +5 -0
- package/dist/src/handlers/desktop-handler.js.map +1 -0
- package/dist/src/handlers/git-handler.d.ts +2 -0
- package/dist/src/handlers/git-handler.js +82 -0
- package/dist/src/handlers/git-handler.js.map +1 -0
- package/dist/src/handlers/index.d.ts +8 -0
- package/dist/src/handlers/index.js +22 -0
- package/dist/src/handlers/index.js.map +1 -0
- package/dist/src/handlers/not-implemented.d.ts +10 -0
- package/dist/src/handlers/not-implemented.js +21 -0
- package/dist/src/handlers/not-implemented.js.map +1 -0
- package/dist/src/handlers/notifications-handler.d.ts +2 -0
- package/dist/src/handlers/notifications-handler.js +62 -0
- package/dist/src/handlers/notifications-handler.js.map +1 -0
- package/dist/src/handlers/params.d.ts +11 -0
- package/dist/src/handlers/params.js +72 -0
- package/dist/src/handlers/params.js.map +1 -0
- package/dist/src/handlers/project-handler.d.ts +2 -0
- package/dist/src/handlers/project-handler.js +6 -0
- package/dist/src/handlers/project-handler.js.map +1 -0
- package/dist/src/handlers/thread-context-handler.d.ts +2 -0
- package/dist/src/handlers/thread-context-handler.js +211 -0
- package/dist/src/handlers/thread-context-handler.js.map +1 -0
- package/dist/src/handlers/workspace-handler.d.ts +2 -0
- package/dist/src/handlers/workspace-handler.js +101 -0
- package/dist/src/handlers/workspace-handler.js.map +1 -0
- package/dist/src/hooks/claude-approval-hook.d.ts +7 -0
- package/dist/src/hooks/claude-approval-hook.js +95 -0
- package/dist/src/hooks/claude-approval-hook.js.map +1 -0
- package/dist/src/hooks/gemini-approval-hook.d.ts +7 -0
- package/dist/src/hooks/gemini-approval-hook.js +113 -0
- package/dist/src/hooks/gemini-approval-hook.js.map +1 -0
- package/dist/src/index.d.ts +62 -0
- package/dist/src/index.js +65 -0
- package/dist/src/index.js.map +1 -0
- package/dist/src/keyring-secret-store.d.ts +36 -0
- package/dist/src/keyring-secret-store.js +70 -0
- package/dist/src/keyring-secret-store.js.map +1 -0
- package/dist/src/lock-file.d.ts +18 -0
- package/dist/src/lock-file.js +60 -0
- package/dist/src/lock-file.js.map +1 -0
- package/dist/src/logger.d.ts +28 -0
- package/dist/src/logger.js +99 -0
- package/dist/src/logger.js.map +1 -0
- package/dist/src/pairing/pairing-code-service.d.ts +45 -0
- package/dist/src/pairing/pairing-code-service.js +183 -0
- package/dist/src/pairing/pairing-code-service.js.map +1 -0
- package/dist/src/projects/project-registry.d.ts +14 -0
- package/dist/src/projects/project-registry.js +60 -0
- package/dist/src/projects/project-registry.js.map +1 -0
- package/dist/src/push/push-sender.d.ts +21 -0
- package/dist/src/push/push-sender.js +96 -0
- package/dist/src/push/push-sender.js.map +1 -0
- package/dist/src/push/push-service.d.ts +122 -0
- package/dist/src/push/push-service.js +260 -0
- package/dist/src/push/push-service.js.map +1 -0
- package/dist/src/qr.d.ts +17 -0
- package/dist/src/qr.js +31 -0
- package/dist/src/qr.js.map +1 -0
- package/dist/src/secret-store.d.ts +23 -0
- package/dist/src/secret-store.js +27 -0
- package/dist/src/secret-store.js.map +1 -0
- package/dist/src/secure-device-state.d.ts +16 -0
- package/dist/src/secure-device-state.js +63 -0
- package/dist/src/secure-device-state.js.map +1 -0
- package/dist/src/service-installer.d.ts +57 -0
- package/dist/src/service-installer.js +254 -0
- package/dist/src/service-installer.js.map +1 -0
- package/dist/src/session-state.d.ts +14 -0
- package/dist/src/session-state.js +19 -0
- package/dist/src/session-state.js.map +1 -0
- package/dist/src/transport/crypto.d.ts +43 -0
- package/dist/src/transport/crypto.js +78 -0
- package/dist/src/transport/crypto.js.map +1 -0
- package/dist/src/transport/lan-server.d.ts +33 -0
- package/dist/src/transport/lan-server.js +105 -0
- package/dist/src/transport/lan-server.js.map +1 -0
- package/dist/src/transport/local-hosts.d.ts +17 -0
- package/dist/src/transport/local-hosts.js +30 -0
- package/dist/src/transport/local-hosts.js.map +1 -0
- package/dist/src/transport/mdns-advertiser.d.ts +83 -0
- package/dist/src/transport/mdns-advertiser.js +282 -0
- package/dist/src/transport/mdns-advertiser.js.map +1 -0
- package/dist/src/transport/message-io.d.ts +33 -0
- package/dist/src/transport/message-io.js +87 -0
- package/dist/src/transport/message-io.js.map +1 -0
- package/dist/src/transport/outbound-log.d.ts +24 -0
- package/dist/src/transport/outbound-log.js +78 -0
- package/dist/src/transport/outbound-log.js.map +1 -0
- package/dist/src/transport/relay-client.d.ts +19 -0
- package/dist/src/transport/relay-client.js +27 -0
- package/dist/src/transport/relay-client.js.map +1 -0
- package/dist/src/transport/secure-channel.d.ts +33 -0
- package/dist/src/transport/secure-channel.js +81 -0
- package/dist/src/transport/secure-channel.js.map +1 -0
- package/dist/src/transport/server-handshake.d.ts +49 -0
- package/dist/src/transport/server-handshake.js +137 -0
- package/dist/src/transport/server-handshake.js.map +1 -0
- package/dist/src/transport/session-handler.d.ts +19 -0
- package/dist/src/transport/session-handler.js +134 -0
- package/dist/src/transport/session-handler.js.map +1 -0
- package/dist/src/transport/session-registry.d.ts +58 -0
- package/dist/src/transport/session-registry.js +91 -0
- package/dist/src/transport/session-registry.js.map +1 -0
- package/dist/src/transport/trust-store.d.ts +23 -0
- package/dist/src/transport/trust-store.js +33 -0
- package/dist/src/transport/trust-store.js.map +1 -0
- package/dist/src/transport/ws-adapter.d.ts +7 -0
- package/dist/src/transport/ws-adapter.js +16 -0
- package/dist/src/transport/ws-adapter.js.map +1 -0
- package/dist/src/version.d.ts +1 -0
- package/dist/src/version.js +13 -0
- package/dist/src/version.js.map +1 -0
- package/dist/src/workspace/browse-service.d.ts +10 -0
- package/dist/src/workspace/browse-service.js +97 -0
- package/dist/src/workspace/browse-service.js.map +1 -0
- package/dist/src/workspace/checkpoint-service.d.ts +21 -0
- package/dist/src/workspace/checkpoint-service.js +219 -0
- package/dist/src/workspace/checkpoint-service.js.map +1 -0
- package/dist/src/workspace/path-guard.d.ts +7 -0
- package/dist/src/workspace/path-guard.js +51 -0
- package/dist/src/workspace/path-guard.js.map +1 -0
- package/dist/src/workspace/workspace-service.d.ts +8 -0
- package/dist/src/workspace/workspace-service.js +111 -0
- package/dist/src/workspace/workspace-service.js.map +1 -0
- package/package.json +46 -0
- package/scripts/extract-gemini-hook.mjs +16 -0
- package/scripts/fake-approval-bridge.mjs +23 -0
- package/scripts/install-service-linux.sh +38 -0
- package/scripts/install-service-macos.sh +38 -0
- package/scripts/install-service-windows.ps1 +26 -0
- package/scripts/test-gemini-hook-e2e.mjs +168 -0
- package/scripts/write-gemini-settings.mjs +31 -0
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Workspace checkpoints: capture / diff / apply a snapshot of the project's
|
|
3
|
+
* working tree, backed by git.
|
|
4
|
+
*
|
|
5
|
+
* A checkpoint is a full snapshot of the working tree (tracked changes AND
|
|
6
|
+
* untracked files) taken WITHOUT touching the user's index: a temporary
|
|
7
|
+
* `GIT_INDEX_FILE` is seeded from HEAD, `git add -A` stages the current tree
|
|
8
|
+
* into it, and `commit-tree` records a snapshot commit parented on HEAD. The
|
|
9
|
+
* commit is anchored under `refs/uxnan/checkpoints/<id>` so git won't GC it, and
|
|
10
|
+
* metadata is persisted in `~/.uxnan/checkpoints.json`.
|
|
11
|
+
*
|
|
12
|
+
* Source: architecture/02a-system-architecture.md §5.8.7.
|
|
13
|
+
*
|
|
14
|
+
* `applyCheckpoint` performs a TRUE worktree restore: it restores the snapshot's
|
|
15
|
+
* file contents (recreating deleted files, overwriting modified ones) AND deletes
|
|
16
|
+
* files created after the checkpoint, so the working tree matches the snapshot
|
|
17
|
+
* exactly (parity with the mobile `AiChangeSet` revert). It is worktree-only —
|
|
18
|
+
* the user's index and HEAD are untouched — and never removes gitignored files
|
|
19
|
+
* (they were excluded from the snapshot).
|
|
20
|
+
*
|
|
21
|
+
* Retention: on each `capture` the service prunes old checkpoints beyond a
|
|
22
|
+
* per-project count cap and/or a TTL, deleting both their `refs/uxnan/checkpoints/*`
|
|
23
|
+
* anchors and their `checkpoints.json` entries, so the set never grows unbounded.
|
|
24
|
+
*
|
|
25
|
+
* Limitation: checkpoint commits use a fixed internal identity (never pushed).
|
|
26
|
+
*/
|
|
27
|
+
import { tmpdir } from 'node:os';
|
|
28
|
+
import { join } from 'node:path';
|
|
29
|
+
import { randomUUID } from 'node:crypto';
|
|
30
|
+
import { rm } from 'node:fs/promises';
|
|
31
|
+
import { JsonRpcErrorCode, RpcError } from '@uxnan/shared';
|
|
32
|
+
import { DAEMON_FILES } from '../daemon-state.js';
|
|
33
|
+
import { runGit } from '../git/git-runner.js';
|
|
34
|
+
const SNAPSHOT_ENV = {
|
|
35
|
+
GIT_AUTHOR_NAME: 'uxnan-bridge',
|
|
36
|
+
GIT_AUTHOR_EMAIL: 'bridge@uxnan.local',
|
|
37
|
+
GIT_COMMITTER_NAME: 'uxnan-bridge',
|
|
38
|
+
GIT_COMMITTER_EMAIL: 'bridge@uxnan.local',
|
|
39
|
+
};
|
|
40
|
+
const DEFAULT_RETENTION = { maxPerProject: 25, ttlDays: 0 };
|
|
41
|
+
const MS_PER_DAY = 86_400_000;
|
|
42
|
+
export class CheckpointService {
|
|
43
|
+
#state;
|
|
44
|
+
#retention;
|
|
45
|
+
constructor(state, retention = {}) {
|
|
46
|
+
this.#state = state;
|
|
47
|
+
this.#retention = { ...DEFAULT_RETENTION, ...retention };
|
|
48
|
+
}
|
|
49
|
+
async capture(cwd, options) {
|
|
50
|
+
const baseSha = (await runGit(cwd, ['rev-parse', 'HEAD'])).stdout.trim();
|
|
51
|
+
const id = randomUUID();
|
|
52
|
+
const indexFile = join(tmpdir(), `uxnan-ckpt-${id}.index`);
|
|
53
|
+
const env = { ...process.env, ...SNAPSHOT_ENV, GIT_INDEX_FILE: indexFile };
|
|
54
|
+
let commitSha;
|
|
55
|
+
try {
|
|
56
|
+
await runGit(cwd, ['read-tree', baseSha], { env });
|
|
57
|
+
await runGit(cwd, ['add', '-A'], { env });
|
|
58
|
+
const treeSha = (await runGit(cwd, ['write-tree'], { env })).stdout.trim();
|
|
59
|
+
const label = options.label ?? 'checkpoint';
|
|
60
|
+
commitSha = (await runGit(cwd, ['commit-tree', treeSha, '-p', baseSha, '-m', `uxnan-checkpoint ${label}`], {
|
|
61
|
+
env,
|
|
62
|
+
})).stdout.trim();
|
|
63
|
+
}
|
|
64
|
+
finally {
|
|
65
|
+
await rm(indexFile, { force: true });
|
|
66
|
+
}
|
|
67
|
+
await runGit(cwd, ['update-ref', `refs/uxnan/checkpoints/${id}`, commitSha]);
|
|
68
|
+
const record = {
|
|
69
|
+
id,
|
|
70
|
+
cwd,
|
|
71
|
+
baseSha,
|
|
72
|
+
commitSha,
|
|
73
|
+
createdAt: options.now,
|
|
74
|
+
...(options.label !== undefined ? { label: options.label } : {}),
|
|
75
|
+
...(options.threadId !== undefined ? { threadId: options.threadId } : {}),
|
|
76
|
+
};
|
|
77
|
+
const records = await this.#list();
|
|
78
|
+
records.push(record);
|
|
79
|
+
const survivors = await this.#prune(records, options.now);
|
|
80
|
+
await this.#state.writeJson(DAEMON_FILES.checkpoints, survivors);
|
|
81
|
+
return toCheckpoint(record);
|
|
82
|
+
}
|
|
83
|
+
async diff(id) {
|
|
84
|
+
const record = await this.#require(id);
|
|
85
|
+
const range = [record.baseSha, record.commitSha];
|
|
86
|
+
const { stdout: diff } = await runGit(record.cwd, ['diff', ...range]);
|
|
87
|
+
const { stdout: nameStatus } = await runGit(record.cwd, ['diff', '--name-status', ...range]);
|
|
88
|
+
const files = nameStatus
|
|
89
|
+
.split('\n')
|
|
90
|
+
.map((line) => line.trim())
|
|
91
|
+
.filter((line) => line.length > 0)
|
|
92
|
+
.map((line) => {
|
|
93
|
+
const parts = line.split('\t');
|
|
94
|
+
const code = parts[0] ?? '';
|
|
95
|
+
const path = parts[parts.length - 1] ?? '';
|
|
96
|
+
return { path, status: mapFileStatus(code) };
|
|
97
|
+
});
|
|
98
|
+
return { diff, files };
|
|
99
|
+
}
|
|
100
|
+
async apply(id) {
|
|
101
|
+
const record = await this.#require(id);
|
|
102
|
+
// Files that exist now but NOT in the snapshot — created after the checkpoint.
|
|
103
|
+
// Compute these BEFORE restoring (which only touches snapshot paths).
|
|
104
|
+
const extras = await this.#extrasOf(record);
|
|
105
|
+
// Restore snapshot contents: recreates deleted files, overwrites modified ones.
|
|
106
|
+
await runGit(record.cwd, ['restore', `--source=${record.commitSha}`, '--', '.']);
|
|
107
|
+
// Delete the extras so the worktree matches the snapshot exactly.
|
|
108
|
+
for (const rel of extras) {
|
|
109
|
+
await rm(join(record.cwd, rel), { force: true });
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Paths present in the current worktree but absent from the checkpoint snapshot
|
|
114
|
+
* (the files to delete on restore). Snapshots the current tree into a temp index
|
|
115
|
+
* exactly like {@link capture} (HEAD + `add -A`, so .gitignore is respected and
|
|
116
|
+
* the user's real index is untouched), then diffs snapshot → now; `A` entries
|
|
117
|
+
* are the extras.
|
|
118
|
+
*/
|
|
119
|
+
async #extrasOf(record) {
|
|
120
|
+
const headSha = (await runGit(record.cwd, ['rev-parse', 'HEAD'])).stdout.trim();
|
|
121
|
+
const indexFile = join(tmpdir(), `uxnan-ckpt-apply-${randomUUID()}.index`);
|
|
122
|
+
const env = { ...process.env, ...SNAPSHOT_ENV, GIT_INDEX_FILE: indexFile };
|
|
123
|
+
try {
|
|
124
|
+
await runGit(record.cwd, ['read-tree', headSha], { env });
|
|
125
|
+
await runGit(record.cwd, ['add', '-A'], { env });
|
|
126
|
+
const nowTree = (await runGit(record.cwd, ['write-tree'], { env })).stdout.trim();
|
|
127
|
+
const { stdout } = await runGit(record.cwd, [
|
|
128
|
+
'diff',
|
|
129
|
+
'--name-status',
|
|
130
|
+
'--no-renames',
|
|
131
|
+
record.commitSha,
|
|
132
|
+
nowTree,
|
|
133
|
+
]);
|
|
134
|
+
const extras = [];
|
|
135
|
+
for (const line of stdout.split('\n')) {
|
|
136
|
+
const trimmed = line.trim();
|
|
137
|
+
if (trimmed.length === 0)
|
|
138
|
+
continue;
|
|
139
|
+
const parts = trimmed.split('\t');
|
|
140
|
+
if ((parts[0] ?? '')[0] !== 'A')
|
|
141
|
+
continue;
|
|
142
|
+
const path = parts[parts.length - 1];
|
|
143
|
+
if (path)
|
|
144
|
+
extras.push(path);
|
|
145
|
+
}
|
|
146
|
+
return extras;
|
|
147
|
+
}
|
|
148
|
+
finally {
|
|
149
|
+
await rm(indexFile, { force: true });
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Drop checkpoints beyond the retention bounds: those older than the TTL, and
|
|
154
|
+
* the oldest-over-cap per project. Deletes each pruned checkpoint's git ref
|
|
155
|
+
* (best-effort — the repo/ref may be gone) and returns the survivors to persist.
|
|
156
|
+
*/
|
|
157
|
+
async #prune(records, now) {
|
|
158
|
+
const { maxPerProject, ttlDays } = this.#retention;
|
|
159
|
+
const ttlCutoff = ttlDays > 0 ? now - ttlDays * MS_PER_DAY : undefined;
|
|
160
|
+
const removeIds = new Set();
|
|
161
|
+
const byCwd = new Map();
|
|
162
|
+
for (const record of records) {
|
|
163
|
+
if (ttlCutoff !== undefined && record.createdAt < ttlCutoff) {
|
|
164
|
+
removeIds.add(record.id);
|
|
165
|
+
continue;
|
|
166
|
+
}
|
|
167
|
+
const list = byCwd.get(record.cwd) ?? [];
|
|
168
|
+
list.push(record);
|
|
169
|
+
byCwd.set(record.cwd, list);
|
|
170
|
+
}
|
|
171
|
+
if (maxPerProject > 0) {
|
|
172
|
+
for (const list of byCwd.values()) {
|
|
173
|
+
list.sort((a, b) => b.createdAt - a.createdAt); // newest first
|
|
174
|
+
for (let i = maxPerProject; i < list.length; i += 1)
|
|
175
|
+
removeIds.add(list[i].id);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
if (removeIds.size === 0)
|
|
179
|
+
return records;
|
|
180
|
+
for (const record of records) {
|
|
181
|
+
if (!removeIds.has(record.id))
|
|
182
|
+
continue;
|
|
183
|
+
try {
|
|
184
|
+
await runGit(record.cwd, ['update-ref', '-d', `refs/uxnan/checkpoints/${record.id}`]);
|
|
185
|
+
}
|
|
186
|
+
catch {
|
|
187
|
+
// The ref or its repo is already gone — nothing to clean up.
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
return records.filter((record) => !removeIds.has(record.id));
|
|
191
|
+
}
|
|
192
|
+
async #list() {
|
|
193
|
+
return (await this.#state.readJson(DAEMON_FILES.checkpoints)) ?? [];
|
|
194
|
+
}
|
|
195
|
+
async #require(id) {
|
|
196
|
+
const record = (await this.#list()).find((r) => r.id === id);
|
|
197
|
+
if (!record) {
|
|
198
|
+
throw new RpcError(JsonRpcErrorCode.ResourceNotFound, `checkpoint not found: ${id}`);
|
|
199
|
+
}
|
|
200
|
+
return record;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
function toCheckpoint(record) {
|
|
204
|
+
const checkpoint = { id: record.id, createdAt: record.createdAt };
|
|
205
|
+
if (record.label !== undefined)
|
|
206
|
+
checkpoint.label = record.label;
|
|
207
|
+
if (record.threadId !== undefined)
|
|
208
|
+
checkpoint.threadId = record.threadId;
|
|
209
|
+
return checkpoint;
|
|
210
|
+
}
|
|
211
|
+
function mapFileStatus(code) {
|
|
212
|
+
const c = code[0];
|
|
213
|
+
if (c === 'A')
|
|
214
|
+
return 'added';
|
|
215
|
+
if (c === 'D')
|
|
216
|
+
return 'deleted';
|
|
217
|
+
return 'modified';
|
|
218
|
+
}
|
|
219
|
+
//# sourceMappingURL=checkpoint-service.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"checkpoint-service.js","sourceRoot":"","sources":["../../../src/workspace/checkpoint-service.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export declare function isSensitiveName(name: string): boolean;
|
|
2
|
+
/**
|
|
3
|
+
* Resolve `relPath` against `root` and ensure the result stays inside `root`.
|
|
4
|
+
* Rejects traversal (`..`), absolute escapes, the `.git` directory and sensitive
|
|
5
|
+
* file names. Returns the absolute path.
|
|
6
|
+
*/
|
|
7
|
+
export declare function resolveWithinRoot(root: string, relPath: string): string;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Path-traversal protection and sensitive-file filtering for workspace access.
|
|
3
|
+
*
|
|
4
|
+
* Source: architecture/02a-system-architecture.md §5.8.9 (sanitization). The
|
|
5
|
+
* bridge never serves files outside the project root, nor secrets (.env, keys,
|
|
6
|
+
* credentials) or the .git internals.
|
|
7
|
+
*/
|
|
8
|
+
import { isAbsolute, relative, resolve, sep } from 'node:path';
|
|
9
|
+
import { JsonRpcErrorCode, RpcError } from '@uxnan/shared';
|
|
10
|
+
const SENSITIVE_PATTERNS = [
|
|
11
|
+
/^\.env(\..+)?$/i,
|
|
12
|
+
/\.pem$/i,
|
|
13
|
+
/\.key$/i,
|
|
14
|
+
/\.p12$/i,
|
|
15
|
+
/\.pfx$/i,
|
|
16
|
+
/\.keystore$/i,
|
|
17
|
+
/^id_rsa/i,
|
|
18
|
+
/^id_ed25519/i,
|
|
19
|
+
/^id_ecdsa/i,
|
|
20
|
+
/^credentials\.json$/i,
|
|
21
|
+
/^\.npmrc$/i,
|
|
22
|
+
];
|
|
23
|
+
export function isSensitiveName(name) {
|
|
24
|
+
return SENSITIVE_PATTERNS.some((pattern) => pattern.test(name));
|
|
25
|
+
}
|
|
26
|
+
function denied(message) {
|
|
27
|
+
return new RpcError(JsonRpcErrorCode.WorkspaceAccessDenied, message);
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Resolve `relPath` against `root` and ensure the result stays inside `root`.
|
|
31
|
+
* Rejects traversal (`..`), absolute escapes, the `.git` directory and sensitive
|
|
32
|
+
* file names. Returns the absolute path.
|
|
33
|
+
*/
|
|
34
|
+
export function resolveWithinRoot(root, relPath) {
|
|
35
|
+
const resolvedRoot = resolve(root);
|
|
36
|
+
const target = resolve(resolvedRoot, relPath);
|
|
37
|
+
const rel = relative(resolvedRoot, target);
|
|
38
|
+
if (rel.startsWith('..') || isAbsolute(rel)) {
|
|
39
|
+
throw denied('path escapes the project root');
|
|
40
|
+
}
|
|
41
|
+
const segments = rel.split(sep);
|
|
42
|
+
if (segments.includes('.git')) {
|
|
43
|
+
throw denied('access to the .git directory is not allowed');
|
|
44
|
+
}
|
|
45
|
+
const name = segments[segments.length - 1] ?? '';
|
|
46
|
+
if (isSensitiveName(name)) {
|
|
47
|
+
throw denied('access to a sensitive file is not allowed');
|
|
48
|
+
}
|
|
49
|
+
return target;
|
|
50
|
+
}
|
|
51
|
+
//# sourceMappingURL=path-guard.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"path-guard.js","sourceRoot":"","sources":["../../../src/workspace/path-guard.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,OAAO,EAAE,UAAU,EAAE,QAAQ,EAAE,OAAO,EAAE,GAAG,EAAE,MAAM,WAAW,CAAC;AAC/D,OAAO,EAAE,gBAAgB,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AAE3D,MAAM,kBAAkB,GAAa;IACnC,iBAAiB;IACjB,SAAS;IACT,SAAS;IACT,SAAS;IACT,SAAS;IACT,cAAc;IACd,UAAU;IACV,cAAc;IACd,YAAY;IACZ,sBAAsB;IACtB,YAAY;CACb,CAAC;AAEF,MAAM,UAAU,eAAe,CAAC,IAAY;IAC1C,OAAO,kBAAkB,CAAC,IAAI,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC;AAClE,CAAC;AAED,SAAS,MAAM,CAAC,OAAe;IAC7B,OAAO,IAAI,QAAQ,CAAC,gBAAgB,CAAC,qBAAqB,EAAE,OAAO,CAAC,CAAC;AACvE,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,iBAAiB,CAAC,IAAY,EAAE,OAAe;IAC7D,MAAM,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACnC,MAAM,MAAM,GAAG,OAAO,CAAC,YAAY,EAAE,OAAO,CAAC,CAAC;IAC9C,MAAM,GAAG,GAAG,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC,CAAC;IAE3C,IAAI,GAAG,CAAC,UAAU,CAAC,IAAI,CAAC,IAAI,UAAU,CAAC,GAAG,CAAC,EAAE,CAAC;QAC5C,MAAM,MAAM,CAAC,+BAA+B,CAAC,CAAC;IAChD,CAAC;IACD,MAAM,QAAQ,GAAG,GAAG,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;IAChC,IAAI,QAAQ,CAAC,QAAQ,CAAC,MAAM,CAAC,EAAE,CAAC;QAC9B,MAAM,MAAM,CAAC,6CAA6C,CAAC,CAAC;IAC9D,CAAC;IACD,MAAM,IAAI,GAAG,QAAQ,CAAC,QAAQ,CAAC,MAAM,GAAG,CAAC,CAAC,IAAI,EAAE,CAAC;IACjD,IAAI,eAAe,CAAC,IAAI,CAAC,EAAE,CAAC;QAC1B,MAAM,MAAM,CAAC,2CAA2C,CAAC,CAAC;IAC5D,CAAC;IACD,OAAO,MAAM,CAAC;AAChB,CAAC"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { ApplyResult, FileContent, ImageContent, PatchChange, WorkspaceListing } from '@uxnan/shared';
|
|
2
|
+
export declare class WorkspaceService {
|
|
3
|
+
#private;
|
|
4
|
+
readFile(root: string, relPath: string): Promise<FileContent>;
|
|
5
|
+
readImage(root: string, relPath: string): Promise<ImageContent>;
|
|
6
|
+
list(root: string): Promise<WorkspaceListing>;
|
|
7
|
+
applyPatch(root: string, changes: PatchChange[]): Promise<ApplyResult>;
|
|
8
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Workspace file operations, confined to the project root and stripped of
|
|
3
|
+
* sensitive files (see {@link resolveWithinRoot}). Paths returned to the phone
|
|
4
|
+
* are relative to the project root, never absolute.
|
|
5
|
+
*
|
|
6
|
+
* Source: architecture/02a-system-architecture.md §5.8.7 / §5.8.9.
|
|
7
|
+
*/
|
|
8
|
+
import { readFile, readdir, stat, mkdir, writeFile, rm } from 'node:fs/promises';
|
|
9
|
+
import { dirname, extname, relative, resolve } from 'node:path';
|
|
10
|
+
import { JsonRpcErrorCode, RpcError } from '@uxnan/shared';
|
|
11
|
+
import { isSensitiveName, resolveWithinRoot } from './path-guard.js';
|
|
12
|
+
const MAX_FILE_BYTES = 5 * 1024 * 1024;
|
|
13
|
+
const MAX_IMAGE_BYTES = 10 * 1024 * 1024;
|
|
14
|
+
const IMAGE_MIME = {
|
|
15
|
+
'.png': 'image/png',
|
|
16
|
+
'.jpg': 'image/jpeg',
|
|
17
|
+
'.jpeg': 'image/jpeg',
|
|
18
|
+
'.gif': 'image/gif',
|
|
19
|
+
'.webp': 'image/webp',
|
|
20
|
+
'.bmp': 'image/bmp',
|
|
21
|
+
'.svg': 'image/svg+xml',
|
|
22
|
+
};
|
|
23
|
+
export class WorkspaceService {
|
|
24
|
+
async readFile(root, relPath) {
|
|
25
|
+
const abs = resolveWithinRoot(root, relPath);
|
|
26
|
+
await this.#assertReadableFile(abs, MAX_FILE_BYTES);
|
|
27
|
+
const buffer = await readFile(abs);
|
|
28
|
+
const path = toRelative(root, abs);
|
|
29
|
+
if (isBinary(buffer)) {
|
|
30
|
+
return { path, content: buffer.toString('base64'), encoding: 'base64' };
|
|
31
|
+
}
|
|
32
|
+
return { path, content: buffer.toString('utf-8'), encoding: 'utf-8' };
|
|
33
|
+
}
|
|
34
|
+
async readImage(root, relPath) {
|
|
35
|
+
const abs = resolveWithinRoot(root, relPath);
|
|
36
|
+
const mimeType = IMAGE_MIME[extname(abs).toLowerCase()];
|
|
37
|
+
if (!mimeType) {
|
|
38
|
+
throw RpcError.invalidParams('not a supported image type');
|
|
39
|
+
}
|
|
40
|
+
await this.#assertReadableFile(abs, MAX_IMAGE_BYTES);
|
|
41
|
+
const buffer = await readFile(abs);
|
|
42
|
+
return { path: toRelative(root, abs), base64Data: buffer.toString('base64'), mimeType };
|
|
43
|
+
}
|
|
44
|
+
async list(root) {
|
|
45
|
+
const resolvedRoot = resolve(root);
|
|
46
|
+
let dirents;
|
|
47
|
+
try {
|
|
48
|
+
dirents = await readdir(resolvedRoot, { withFileTypes: true });
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
throw new RpcError(JsonRpcErrorCode.WorkspaceAccessDenied, 'directory not accessible');
|
|
52
|
+
}
|
|
53
|
+
const entries = [];
|
|
54
|
+
for (const dirent of dirents) {
|
|
55
|
+
if (dirent.name === '.git' || isSensitiveName(dirent.name))
|
|
56
|
+
continue;
|
|
57
|
+
const isDir = dirent.isDirectory();
|
|
58
|
+
const entry = { name: dirent.name, type: isDir ? 'dir' : 'file' };
|
|
59
|
+
if (!isDir) {
|
|
60
|
+
try {
|
|
61
|
+
entry.size = (await stat(resolve(resolvedRoot, dirent.name))).size;
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
// ignore unreadable entries' size
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
entries.push(entry);
|
|
68
|
+
}
|
|
69
|
+
entries.sort((a, b) => a.type === b.type ? a.name.localeCompare(b.name) : a.type === 'dir' ? -1 : 1);
|
|
70
|
+
return { cwd: '.', entries };
|
|
71
|
+
}
|
|
72
|
+
async applyPatch(root, changes) {
|
|
73
|
+
let applied = 0;
|
|
74
|
+
for (const change of changes) {
|
|
75
|
+
const abs = resolveWithinRoot(root, change.path);
|
|
76
|
+
if (change.op === 'delete') {
|
|
77
|
+
await rm(abs, { force: true });
|
|
78
|
+
applied += 1;
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
// add | modify
|
|
82
|
+
await mkdir(dirname(abs), { recursive: true });
|
|
83
|
+
await writeFile(abs, change.content ?? '', 'utf-8');
|
|
84
|
+
applied += 1;
|
|
85
|
+
}
|
|
86
|
+
return { success: true, applied };
|
|
87
|
+
}
|
|
88
|
+
async #assertReadableFile(abs, maxBytes) {
|
|
89
|
+
let info;
|
|
90
|
+
try {
|
|
91
|
+
info = await stat(abs);
|
|
92
|
+
}
|
|
93
|
+
catch {
|
|
94
|
+
throw new RpcError(JsonRpcErrorCode.ResourceNotFound, 'file not found');
|
|
95
|
+
}
|
|
96
|
+
if (!info.isFile()) {
|
|
97
|
+
throw RpcError.invalidParams('path is not a file');
|
|
98
|
+
}
|
|
99
|
+
if (info.size > maxBytes) {
|
|
100
|
+
throw new RpcError(JsonRpcErrorCode.BridgeError, 'file is too large to read');
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
function toRelative(root, abs) {
|
|
105
|
+
return relative(resolve(root), abs).split('\\').join('/');
|
|
106
|
+
}
|
|
107
|
+
function isBinary(buffer) {
|
|
108
|
+
const sample = buffer.subarray(0, 8000);
|
|
109
|
+
return sample.includes(0);
|
|
110
|
+
}
|
|
111
|
+
//# sourceMappingURL=workspace-service.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"workspace-service.js","sourceRoot":"","sources":["../../../src/workspace/workspace-service.ts"],"names":[],"mappings":"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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "uxnan-bridge",
|
|
3
|
+
"version": "0.0.1-alpha.20260621",
|
|
4
|
+
"description": "Uxnan Bridge — local control-plane daemon connecting the Uxnan mobile app to the developer's PC over E2EE.",
|
|
5
|
+
"license": "MPL-2.0",
|
|
6
|
+
"author": "Luis Donaldo Gamas Vazquez",
|
|
7
|
+
"type": "module",
|
|
8
|
+
"main": "./dist/src/index.js",
|
|
9
|
+
"types": "./dist/src/index.d.ts",
|
|
10
|
+
"bin": {
|
|
11
|
+
"uxnan-bridge": "dist/src/cli.js"
|
|
12
|
+
},
|
|
13
|
+
"files": [
|
|
14
|
+
"dist/src",
|
|
15
|
+
"scripts"
|
|
16
|
+
],
|
|
17
|
+
"engines": {
|
|
18
|
+
"node": ">=18"
|
|
19
|
+
},
|
|
20
|
+
"repository": {
|
|
21
|
+
"type": "git",
|
|
22
|
+
"url": "git+https://github.com/luisgamas/uxnan.git",
|
|
23
|
+
"directory": "bridge"
|
|
24
|
+
},
|
|
25
|
+
"scripts": {
|
|
26
|
+
"build": "tsc",
|
|
27
|
+
"typecheck": "tsc --noEmit",
|
|
28
|
+
"test": "tsc && node --test --test-concurrency=1 \"dist/test/**/*.test.js\"",
|
|
29
|
+
"start": "node dist/src/cli.js start",
|
|
30
|
+
"prepublishOnly": "tsc"
|
|
31
|
+
},
|
|
32
|
+
"dependencies": {
|
|
33
|
+
"@uxnan/shared": "0.0.1-alpha.20260621",
|
|
34
|
+
"ajv": "^8.17.1",
|
|
35
|
+
"qrcode-terminal": "^0.12.0",
|
|
36
|
+
"ws": "^8.18.0"
|
|
37
|
+
},
|
|
38
|
+
"devDependencies": {
|
|
39
|
+
"@types/ws": "^8.5.12",
|
|
40
|
+
"uxnan-relay": "*"
|
|
41
|
+
},
|
|
42
|
+
"optionalDependencies": {
|
|
43
|
+
"@napi-rs/keyring": "^1.3.0",
|
|
44
|
+
"firebase-admin": "^13.0.0"
|
|
45
|
+
}
|
|
46
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Writes the bridge's Gemini approval hook to a path passed on argv[2],
|
|
3
|
+
* then exits. Used by the end-to-end smoke test against the real `gemini`
|
|
4
|
+
* CLI to materialize the actual CJS script the bridge ships.
|
|
5
|
+
*/
|
|
6
|
+
import { writeGeminiApprovalHook } from '../dist/src/hooks/gemini-approval-hook.js';
|
|
7
|
+
import { writeFileSync } from 'node:fs';
|
|
8
|
+
|
|
9
|
+
const path = process.argv[2];
|
|
10
|
+
if (!path) {
|
|
11
|
+
console.error('usage: extract-gemini-hook.mjs <output-path>');
|
|
12
|
+
process.exit(2);
|
|
13
|
+
}
|
|
14
|
+
const written = await writeGeminiApprovalHook(path);
|
|
15
|
+
writeFileSync(written + '.sha', 'sentinel', 'utf-8');
|
|
16
|
+
console.log(written);
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Spins up a fake bridge HTTP endpoint that ALWAYS returns `allow` after a
|
|
3
|
+
* short delay (so the gemini CLI's tool call gets a real round-trip), then
|
|
4
|
+
* prints the request it received and exits.
|
|
5
|
+
*/
|
|
6
|
+
import { createServer } from 'node:http';
|
|
7
|
+
|
|
8
|
+
const server = createServer((req, res) => {
|
|
9
|
+
let body = '';
|
|
10
|
+
req.on('data', (c) => (body += c));
|
|
11
|
+
req.on('end', () => {
|
|
12
|
+
console.log(`[bridge] ${req.method} ${req.url} body=${body}`);
|
|
13
|
+
// Mimic the real bridge: hold the response for 250 ms (so the hook waits,
|
|
14
|
+
// exercising the same code path the real phone would) then return `allow`.
|
|
15
|
+
setTimeout(() => {
|
|
16
|
+
res.writeHead(200, { 'content-type': 'application/json' });
|
|
17
|
+
res.end(JSON.stringify({ decision: 'allow' }));
|
|
18
|
+
}, 250);
|
|
19
|
+
});
|
|
20
|
+
});
|
|
21
|
+
server.listen(19999, '127.0.0.1', () => {
|
|
22
|
+
console.log('[bridge] listening on http://127.0.0.1:19999/agent-hook/approval');
|
|
23
|
+
});
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# install-service-linux.sh
|
|
3
|
+
#
|
|
4
|
+
# Installs a systemd USER unit so the uxnan-bridge daemon starts at login.
|
|
5
|
+
# Requires the global CLI: npm install -g uxnan-bridge
|
|
6
|
+
#
|
|
7
|
+
# Remove: systemctl --user disable --now uxnan-bridge.service && \
|
|
8
|
+
# rm "${XDG_CONFIG_HOME:-$HOME/.config}/systemd/user/uxnan-bridge.service"
|
|
9
|
+
set -euo pipefail
|
|
10
|
+
|
|
11
|
+
BIN="$(command -v uxnan-bridge || true)"
|
|
12
|
+
if [ -z "$BIN" ]; then
|
|
13
|
+
echo "uxnan-bridge not found on PATH. Run: npm install -g uxnan-bridge" >&2
|
|
14
|
+
exit 1
|
|
15
|
+
fi
|
|
16
|
+
|
|
17
|
+
UNIT_DIR="${XDG_CONFIG_HOME:-$HOME/.config}/systemd/user"
|
|
18
|
+
mkdir -p "$UNIT_DIR"
|
|
19
|
+
|
|
20
|
+
cat > "$UNIT_DIR/uxnan-bridge.service" <<EOF
|
|
21
|
+
[Unit]
|
|
22
|
+
Description=Uxnan Bridge daemon
|
|
23
|
+
After=network-online.target
|
|
24
|
+
Wants=network-online.target
|
|
25
|
+
|
|
26
|
+
[Service]
|
|
27
|
+
ExecStart=$BIN start
|
|
28
|
+
Restart=on-failure
|
|
29
|
+
RestartSec=5
|
|
30
|
+
|
|
31
|
+
[Install]
|
|
32
|
+
WantedBy=default.target
|
|
33
|
+
EOF
|
|
34
|
+
|
|
35
|
+
systemctl --user daemon-reload
|
|
36
|
+
systemctl --user enable --now uxnan-bridge.service
|
|
37
|
+
echo "Enabled systemd user unit uxnan-bridge.service."
|
|
38
|
+
echo "Tip: 'loginctl enable-linger \$USER' keeps it running after logout."
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# install-service-macos.sh
|
|
3
|
+
#
|
|
4
|
+
# Installs a LaunchAgent so the uxnan-bridge daemon starts at login.
|
|
5
|
+
# Requires the global CLI: npm install -g uxnan-bridge
|
|
6
|
+
#
|
|
7
|
+
# Remove: launchctl unload ~/Library/LaunchAgents/dev.luisgamas.bridge.plist && \
|
|
8
|
+
# rm ~/Library/LaunchAgents/dev.luisgamas.bridge.plist
|
|
9
|
+
set -euo pipefail
|
|
10
|
+
|
|
11
|
+
BIN="$(command -v uxnan-bridge || true)"
|
|
12
|
+
if [ -z "$BIN" ]; then
|
|
13
|
+
echo "uxnan-bridge not found on PATH. Run: npm install -g uxnan-bridge" >&2
|
|
14
|
+
exit 1
|
|
15
|
+
fi
|
|
16
|
+
|
|
17
|
+
mkdir -p "$HOME/Library/LaunchAgents" "$HOME/.uxnan/logs"
|
|
18
|
+
PLIST="$HOME/Library/LaunchAgents/dev.luisgamas.bridge.plist"
|
|
19
|
+
|
|
20
|
+
cat > "$PLIST" <<EOF
|
|
21
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
22
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
23
|
+
<plist version="1.0">
|
|
24
|
+
<dict>
|
|
25
|
+
<key>Label</key><string>dev.luisgamas.bridge</string>
|
|
26
|
+
<key>ProgramArguments</key>
|
|
27
|
+
<array><string>$BIN</string><string>start</string></array>
|
|
28
|
+
<key>RunAtLoad</key><true/>
|
|
29
|
+
<key>KeepAlive</key><true/>
|
|
30
|
+
<key>StandardOutPath</key><string>$HOME/.uxnan/logs/launchd.out.log</string>
|
|
31
|
+
<key>StandardErrorPath</key><string>$HOME/.uxnan/logs/launchd.err.log</string>
|
|
32
|
+
</dict>
|
|
33
|
+
</plist>
|
|
34
|
+
EOF
|
|
35
|
+
|
|
36
|
+
launchctl unload "$PLIST" 2>/dev/null || true
|
|
37
|
+
launchctl load "$PLIST"
|
|
38
|
+
echo "Loaded LaunchAgent dev.luisgamas.bridge (starts at login)."
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# install-service-windows.ps1
|
|
2
|
+
#
|
|
3
|
+
# Registers the uxnan-bridge daemon to start at user logon via Task Scheduler.
|
|
4
|
+
# Requires the global CLI: npm install -g uxnan-bridge
|
|
5
|
+
#
|
|
6
|
+
# Usage: powershell -ExecutionPolicy Bypass -File install-service-windows.ps1
|
|
7
|
+
# Remove: Unregister-ScheduledTask -TaskName 'UxnanBridge' -Confirm:$false
|
|
8
|
+
|
|
9
|
+
$ErrorActionPreference = 'Stop'
|
|
10
|
+
|
|
11
|
+
$bin = (Get-Command uxnan-bridge -ErrorAction SilentlyContinue).Source
|
|
12
|
+
if (-not $bin) {
|
|
13
|
+
Write-Error "uxnan-bridge not found on PATH. Run: npm install -g uxnan-bridge"
|
|
14
|
+
exit 1
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
$action = New-ScheduledTaskAction -Execute $bin -Argument 'start'
|
|
18
|
+
$trigger = New-ScheduledTaskTrigger -AtLogOn
|
|
19
|
+
$settings = New-ScheduledTaskSettingsSet -StartWhenAvailable `
|
|
20
|
+
-RestartCount 3 -RestartInterval (New-TimeSpan -Minutes 1) -ExecutionTimeLimit ([TimeSpan]::Zero)
|
|
21
|
+
|
|
22
|
+
Register-ScheduledTask -TaskName 'UxnanBridge' -Action $action -Trigger $trigger `
|
|
23
|
+
-Settings $settings -Description 'Uxnan Bridge daemon' -Force | Out-Null
|
|
24
|
+
|
|
25
|
+
Write-Host "Registered scheduled task 'UxnanBridge' (starts at logon)."
|
|
26
|
+
Write-Host "Start now with: Start-ScheduledTask -TaskName 'UxnanBridge'"
|