@buildifyx/desktop-agent 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.
Files changed (50) hide show
  1. package/README.md +337 -0
  2. package/package.json +32 -0
  3. package/src/audit/logger.js +30 -0
  4. package/src/cli/commands/cloud.js +89 -0
  5. package/src/cli/commands/doctor.js +18 -0
  6. package/src/cli/commands/login.js +59 -0
  7. package/src/cli/commands/logout.js +20 -0
  8. package/src/cli/commands/remote.js +64 -0
  9. package/src/cli/commands/status.js +32 -0
  10. package/src/cli/commands/update.js +31 -0
  11. package/src/cli/help.js +52 -0
  12. package/src/cli/main.js +55 -0
  13. package/src/cli/options.js +34 -0
  14. package/src/cli.js +20 -0
  15. package/src/core/dispatcher.js +49 -0
  16. package/src/core/errors.js +37 -0
  17. package/src/core/permissions.js +53 -0
  18. package/src/core/runtime.js +45 -0
  19. package/src/events/bus.js +39 -0
  20. package/src/permissions/approvals.js +47 -0
  21. package/src/permissions/controller.js +92 -0
  22. package/src/permissions/evaluator.js +93 -0
  23. package/src/permissions/manager.js +57 -0
  24. package/src/permissions/policy.js +28 -0
  25. package/src/permissions/store.js +27 -0
  26. package/src/security/path.js +61 -0
  27. package/src/security/scope.js +45 -0
  28. package/src/server.js +1 -0
  29. package/src/services/commands.js +107 -0
  30. package/src/services/files.js +104 -0
  31. package/src/services/index.js +11 -0
  32. package/src/services/system.js +17 -0
  33. package/src/transport/cloud.js +206 -0
  34. package/src/transport/mcp/manifest-store.js +32 -0
  35. package/src/transport/mcp/response.js +25 -0
  36. package/src/transport/mcp/server.js +114 -0
  37. package/src/transport/mcp/tools/commands.js +33 -0
  38. package/src/transport/mcp/tools/files.js +55 -0
  39. package/src/transport/mcp/tools/index.js +1 -0
  40. package/src/transport/mcp/tools/registry.js +92 -0
  41. package/src/transport/mcp/tools/system.js +16 -0
  42. package/src/tui/app.js +392 -0
  43. package/src/tui/commands.js +71 -0
  44. package/src/tui/index.js +37 -0
  45. package/src/tui/layout.js +48 -0
  46. package/src/tui/model.js +62 -0
  47. package/src/tui/profiles.js +32 -0
  48. package/src/utils/credentials.js +35 -0
  49. package/src/utils/text.js +180 -0
  50. package/src/version.js +111 -0
@@ -0,0 +1,35 @@
1
+ import { chmod, mkdir, readFile, rm, writeFile } from 'node:fs/promises';
2
+ import os from 'node:os';
3
+ import path from 'node:path';
4
+
5
+ export const DEFAULT_CREDENTIALS_FILE = path.join(os.homedir(), '.buildifyx', 'credentials.json');
6
+
7
+ export async function loadCredentials(filePath = DEFAULT_CREDENTIALS_FILE) {
8
+ try {
9
+ const raw = await readFile(filePath, 'utf8');
10
+ return JSON.parse(raw);
11
+ } catch (error) {
12
+ if (error?.code === 'ENOENT') return null;
13
+ throw error;
14
+ }
15
+ }
16
+
17
+ export async function saveCredentials(credentials, filePath = DEFAULT_CREDENTIALS_FILE) {
18
+ await mkdir(path.dirname(filePath), { recursive: true });
19
+ const temp = `${filePath}.tmp-${process.pid}-${Date.now()}`;
20
+ await writeFile(temp, `${JSON.stringify(credentials, null, 2)}\n`, { encoding: 'utf8', mode: 0o600 });
21
+ await chmod(temp, 0o600).catch(() => {});
22
+ await import('node:fs/promises').then(({ rename }) => rename(temp, filePath));
23
+ await chmod(filePath, 0o600).catch(() => {});
24
+ return filePath;
25
+ }
26
+
27
+ export async function clearCredentials(filePath = DEFAULT_CREDENTIALS_FILE) {
28
+ await rm(filePath, { force: true });
29
+ }
30
+
31
+ export function isCredentialExpired(credentials, now = Date.now()) {
32
+ if (!credentials?.expiresAt) return false;
33
+ const expiry = Date.parse(credentials.expiresAt);
34
+ return Number.isFinite(expiry) && expiry <= now;
35
+ }
@@ -0,0 +1,180 @@
1
+ import { constants } from 'node:fs';
2
+ import { access, open, rename, stat, unlink } from 'node:fs/promises';
3
+ import path from 'node:path';
4
+ import process from 'node:process';
5
+ import { randomUUID } from 'node:crypto';
6
+
7
+ export const MAX_TEXT_FILE_BYTES = 1024 * 1024;
8
+
9
+ const utf8Decoder = new TextDecoder('utf-8', {
10
+ fatal: true,
11
+ ignoreBOM: true
12
+ });
13
+
14
+ export async function readUtf8File(filePath) {
15
+ const fileStat = await stat(filePath);
16
+
17
+ if (!fileStat.isFile()) {
18
+ throw new Error('The requested path is not a regular file.');
19
+ }
20
+ if (fileStat.size > MAX_TEXT_FILE_BYTES) {
21
+ throw new Error(`File exceeds the ${MAX_TEXT_FILE_BYTES}-byte limit.`);
22
+ }
23
+
24
+ const handle = await open(filePath, 'r');
25
+ try {
26
+ const buffer = await handle.readFile();
27
+ if (buffer.byteLength > MAX_TEXT_FILE_BYTES) {
28
+ throw new Error(`File exceeds the ${MAX_TEXT_FILE_BYTES}-byte limit.`);
29
+ }
30
+
31
+ try {
32
+ return utf8Decoder.decode(buffer);
33
+ } catch {
34
+ throw new Error('The requested file is not valid UTF-8 text.');
35
+ }
36
+ } finally {
37
+ await handle.close();
38
+ }
39
+ }
40
+
41
+ function getLines(content) {
42
+ const lines = [];
43
+ const newlinePattern = /\r\n|\n|\r/g;
44
+ let start = 0;
45
+ let match;
46
+
47
+ while ((match = newlinePattern.exec(content)) !== null) {
48
+ lines.push({
49
+ start,
50
+ contentEnd: match.index,
51
+ end: newlinePattern.lastIndex,
52
+ separator: match[0]
53
+ });
54
+ start = newlinePattern.lastIndex;
55
+ }
56
+
57
+ lines.push({ start, contentEnd: content.length, end: content.length, separator: '' });
58
+ return lines;
59
+ }
60
+
61
+ export function countTextLines(content) {
62
+ return getLines(content).length;
63
+ }
64
+
65
+ function assertLineNumber(lines, line, label) {
66
+ if (line < 1 || line > lines.length) {
67
+ throw new Error(`${label} must be between 1 and ${lines.length}.`);
68
+ }
69
+ }
70
+
71
+ export function replaceLines(content, startLine, endLine, replacementLines) {
72
+ const lines = getLines(content);
73
+ assertLineNumber(lines, startLine, 'startLine');
74
+ assertLineNumber(lines, endLine, 'endLine');
75
+
76
+ if (endLine < startLine) {
77
+ throw new Error('endLine must be greater than or equal to startLine.');
78
+ }
79
+ if (replacementLines.some((line) => /[\r\n]/.test(line))) {
80
+ throw new Error('Each replacement line must not contain newline characters.');
81
+ }
82
+
83
+ const first = lines[startLine - 1];
84
+ const last = lines[endLine - 1];
85
+ const preferredSeparator = lines.find((line) => line.separator)?.separator ?? '\n';
86
+ let replacement = replacementLines.join(preferredSeparator);
87
+
88
+ if (replacementLines.length > 0 && last.separator) {
89
+ replacement += last.separator;
90
+ }
91
+
92
+ return content.slice(0, first.start) + replacement + content.slice(last.end);
93
+ }
94
+
95
+ function characterOffset(content, position, label) {
96
+ const lines = getLines(content);
97
+ assertLineNumber(lines, position.line, `${label}.line`);
98
+
99
+ const selectedLine = lines[position.line - 1];
100
+ const lineContent = content.slice(selectedLine.start, selectedLine.contentEnd);
101
+ const characters = Array.from(lineContent);
102
+ const maxColumn = characters.length + 1;
103
+
104
+ if (position.column < 1 || position.column > maxColumn) {
105
+ throw new Error(`${label}.column must be between 1 and ${maxColumn}.`);
106
+ }
107
+
108
+ return selectedLine.start + characters.slice(0, position.column - 1).join('').length;
109
+ }
110
+
111
+ export function replaceCharacters(content, start, end, replacement) {
112
+ const startOffset = characterOffset(content, start, 'start');
113
+ const endOffset = characterOffset(content, end, 'end');
114
+
115
+ if (endOffset < startOffset) {
116
+ throw new Error('The end position must not come before the start position.');
117
+ }
118
+
119
+ return content.slice(0, startOffset) + replacement + content.slice(endOffset);
120
+ }
121
+
122
+ export async function createUtf8File(filePath, content) {
123
+ const byteLength = Buffer.byteLength(content, 'utf8');
124
+ if (byteLength > MAX_TEXT_FILE_BYTES) {
125
+ throw new Error(`File exceeds the ${MAX_TEXT_FILE_BYTES}-byte limit.`);
126
+ }
127
+
128
+ let handle;
129
+ try {
130
+ handle = await open(filePath, 'wx', 0o644);
131
+ await handle.writeFile(content, 'utf8');
132
+ await handle.sync();
133
+ } catch (error) {
134
+ if (error && typeof error === 'object' && error.code === 'EEXIST') {
135
+ throw new Error('File already exists.');
136
+ }
137
+ throw error;
138
+ } finally {
139
+ if (handle) {
140
+ await handle.close();
141
+ }
142
+ }
143
+ }
144
+
145
+ export async function writeUtf8FileAtomic(filePath, content) {
146
+ const byteLength = Buffer.byteLength(content, 'utf8');
147
+ if (byteLength > MAX_TEXT_FILE_BYTES) {
148
+ throw new Error(`Edited file exceeds the ${MAX_TEXT_FILE_BYTES}-byte limit.`);
149
+ }
150
+
151
+ const originalStat = await stat(filePath);
152
+ if (!originalStat.isFile()) {
153
+ throw new Error('The requested path is not a regular file.');
154
+ }
155
+ await access(filePath, constants.W_OK);
156
+
157
+ const directory = path.dirname(filePath);
158
+ const basename = path.basename(filePath);
159
+ const temporaryPath = path.join(directory, `.${basename}.buildifyx-${process.pid}-${randomUUID()}.tmp`);
160
+ let temporaryCreated = false;
161
+
162
+ try {
163
+ const handle = await open(temporaryPath, 'wx', originalStat.mode);
164
+ temporaryCreated = true;
165
+ try {
166
+ await handle.writeFile(content, 'utf8');
167
+ await handle.chmod(originalStat.mode);
168
+ await handle.sync();
169
+ } finally {
170
+ await handle.close();
171
+ }
172
+
173
+ await rename(temporaryPath, filePath);
174
+ temporaryCreated = false;
175
+ } finally {
176
+ if (temporaryCreated) {
177
+ await unlink(temporaryPath).catch(() => undefined);
178
+ }
179
+ }
180
+ }
package/src/version.js ADDED
@@ -0,0 +1,111 @@
1
+ import { readFile } from 'node:fs/promises';
2
+
3
+ const packageJsonUrl = new URL('../package.json', import.meta.url);
4
+
5
+ export async function getPackageMetadata() {
6
+ const content = await readFile(packageJsonUrl, 'utf8');
7
+ const metadata = JSON.parse(content);
8
+
9
+ return {
10
+ name: metadata.name,
11
+ version: metadata.version
12
+ };
13
+ }
14
+
15
+ function parseVersion(version) {
16
+ const normalized = version.trim().replace(/^v/, '');
17
+ const [core, prerelease = ''] = normalized.split('-', 2);
18
+ const parts = core.split('.').map((part) => Number(part));
19
+
20
+ if (parts.length !== 3 || parts.some((part) => !Number.isInteger(part) || part < 0)) {
21
+ return null;
22
+ }
23
+
24
+ return {
25
+ core: parts,
26
+ prerelease: prerelease ? prerelease.split('.') : []
27
+ };
28
+ }
29
+
30
+ function comparePrerelease(left, right) {
31
+ if (left.length === 0 && right.length === 0) return 0;
32
+ if (left.length === 0) return 1;
33
+ if (right.length === 0) return -1;
34
+
35
+ const length = Math.max(left.length, right.length);
36
+ for (let index = 0; index < length; index += 1) {
37
+ const a = left[index];
38
+ const b = right[index];
39
+
40
+ if (a === undefined) return -1;
41
+ if (b === undefined) return 1;
42
+ if (a === b) continue;
43
+
44
+ const aNumber = /^\d+$/.test(a) ? Number(a) : null;
45
+ const bNumber = /^\d+$/.test(b) ? Number(b) : null;
46
+
47
+ if (aNumber !== null && bNumber !== null) return aNumber > bNumber ? 1 : -1;
48
+ if (aNumber !== null) return -1;
49
+ if (bNumber !== null) return 1;
50
+ return a > b ? 1 : -1;
51
+ }
52
+
53
+ return 0;
54
+ }
55
+
56
+ export function compareVersions(leftVersion, rightVersion) {
57
+ const left = parseVersion(leftVersion);
58
+ const right = parseVersion(rightVersion);
59
+
60
+ if (!left || !right) {
61
+ return leftVersion === rightVersion ? 0 : null;
62
+ }
63
+
64
+ for (let index = 0; index < 3; index += 1) {
65
+ if (left.core[index] > right.core[index]) return 1;
66
+ if (left.core[index] < right.core[index]) return -1;
67
+ }
68
+
69
+ return comparePrerelease(left.prerelease, right.prerelease);
70
+ }
71
+
72
+ export async function getLatestNpmVersion(packageName, { timeoutMs = 2000 } = {}) {
73
+ const encodedName = packageName.replace('/', '%2f');
74
+ const response = await fetch(`https://registry.npmjs.org/${encodedName}/latest`, {
75
+ headers: {
76
+ accept: 'application/json'
77
+ },
78
+ signal: AbortSignal.timeout(timeoutMs)
79
+ });
80
+
81
+ if (!response.ok) {
82
+ throw new Error(`npm registry returned HTTP ${response.status}`);
83
+ }
84
+
85
+ const metadata = await response.json();
86
+ if (!metadata || typeof metadata.version !== 'string' || !metadata.version) {
87
+ throw new Error('npm registry response does not contain a version.');
88
+ }
89
+
90
+ return metadata.version;
91
+ }
92
+
93
+ export async function checkForUpdate(packageName, currentVersion, options) {
94
+ try {
95
+ const latestVersion = await getLatestNpmVersion(packageName, options);
96
+ const comparison = compareVersions(latestVersion, currentVersion);
97
+
98
+ return {
99
+ currentVersion,
100
+ latestVersion,
101
+ updateAvailable: comparison === 1
102
+ };
103
+ } catch (error) {
104
+ return {
105
+ currentVersion,
106
+ latestVersion: null,
107
+ updateAvailable: false,
108
+ error: error instanceof Error ? error.message : String(error)
109
+ };
110
+ }
111
+ }