ctx-sync 1.0.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 (145) hide show
  1. package/dist/commands/audit.d.ts +76 -0
  2. package/dist/commands/audit.d.ts.map +1 -0
  3. package/dist/commands/audit.js +367 -0
  4. package/dist/commands/audit.js.map +1 -0
  5. package/dist/commands/config.d.ts +58 -0
  6. package/dist/commands/config.d.ts.map +1 -0
  7. package/dist/commands/config.js +114 -0
  8. package/dist/commands/config.js.map +1 -0
  9. package/dist/commands/dir.d.ts +56 -0
  10. package/dist/commands/dir.d.ts.map +1 -0
  11. package/dist/commands/dir.js +172 -0
  12. package/dist/commands/dir.js.map +1 -0
  13. package/dist/commands/docker.d.ts +140 -0
  14. package/dist/commands/docker.d.ts.map +1 -0
  15. package/dist/commands/docker.js +380 -0
  16. package/dist/commands/docker.js.map +1 -0
  17. package/dist/commands/env.d.ts +96 -0
  18. package/dist/commands/env.d.ts.map +1 -0
  19. package/dist/commands/env.js +352 -0
  20. package/dist/commands/env.js.map +1 -0
  21. package/dist/commands/init.d.ts +89 -0
  22. package/dist/commands/init.d.ts.map +1 -0
  23. package/dist/commands/init.js +272 -0
  24. package/dist/commands/init.js.map +1 -0
  25. package/dist/commands/key.d.ts +92 -0
  26. package/dist/commands/key.d.ts.map +1 -0
  27. package/dist/commands/key.js +274 -0
  28. package/dist/commands/key.js.map +1 -0
  29. package/dist/commands/list.d.ts +38 -0
  30. package/dist/commands/list.d.ts.map +1 -0
  31. package/dist/commands/list.js +84 -0
  32. package/dist/commands/list.js.map +1 -0
  33. package/dist/commands/note.d.ts +151 -0
  34. package/dist/commands/note.d.ts.map +1 -0
  35. package/dist/commands/note.js +411 -0
  36. package/dist/commands/note.js.map +1 -0
  37. package/dist/commands/pull.d.ts +47 -0
  38. package/dist/commands/pull.d.ts.map +1 -0
  39. package/dist/commands/pull.js +94 -0
  40. package/dist/commands/pull.js.map +1 -0
  41. package/dist/commands/push.d.ts +40 -0
  42. package/dist/commands/push.d.ts.map +1 -0
  43. package/dist/commands/push.js +94 -0
  44. package/dist/commands/push.js.map +1 -0
  45. package/dist/commands/restore.d.ts +116 -0
  46. package/dist/commands/restore.d.ts.map +1 -0
  47. package/dist/commands/restore.js +336 -0
  48. package/dist/commands/restore.js.map +1 -0
  49. package/dist/commands/service.d.ts +83 -0
  50. package/dist/commands/service.d.ts.map +1 -0
  51. package/dist/commands/service.js +259 -0
  52. package/dist/commands/service.js.map +1 -0
  53. package/dist/commands/show.d.ts +63 -0
  54. package/dist/commands/show.d.ts.map +1 -0
  55. package/dist/commands/show.js +243 -0
  56. package/dist/commands/show.js.map +1 -0
  57. package/dist/commands/status.d.ts +53 -0
  58. package/dist/commands/status.d.ts.map +1 -0
  59. package/dist/commands/status.js +150 -0
  60. package/dist/commands/status.js.map +1 -0
  61. package/dist/commands/sync.d.ts +105 -0
  62. package/dist/commands/sync.d.ts.map +1 -0
  63. package/dist/commands/sync.js +243 -0
  64. package/dist/commands/sync.js.map +1 -0
  65. package/dist/commands/team.d.ts +79 -0
  66. package/dist/commands/team.d.ts.map +1 -0
  67. package/dist/commands/team.js +233 -0
  68. package/dist/commands/team.js.map +1 -0
  69. package/dist/commands/track.d.ts +109 -0
  70. package/dist/commands/track.d.ts.map +1 -0
  71. package/dist/commands/track.js +406 -0
  72. package/dist/commands/track.js.map +1 -0
  73. package/dist/core/command-validator.d.ts +100 -0
  74. package/dist/core/command-validator.d.ts.map +1 -0
  75. package/dist/core/command-validator.js +299 -0
  76. package/dist/core/command-validator.js.map +1 -0
  77. package/dist/core/config-store.d.ts +76 -0
  78. package/dist/core/config-store.d.ts.map +1 -0
  79. package/dist/core/config-store.js +148 -0
  80. package/dist/core/config-store.js.map +1 -0
  81. package/dist/core/directories-handler.d.ts +116 -0
  82. package/dist/core/directories-handler.d.ts.map +1 -0
  83. package/dist/core/directories-handler.js +199 -0
  84. package/dist/core/directories-handler.js.map +1 -0
  85. package/dist/core/docker-handler.d.ts +183 -0
  86. package/dist/core/docker-handler.d.ts.map +1 -0
  87. package/dist/core/docker-handler.js +515 -0
  88. package/dist/core/docker-handler.js.map +1 -0
  89. package/dist/core/encryption.d.ts +79 -0
  90. package/dist/core/encryption.d.ts.map +1 -0
  91. package/dist/core/encryption.js +111 -0
  92. package/dist/core/encryption.js.map +1 -0
  93. package/dist/core/env-handler.d.ts +128 -0
  94. package/dist/core/env-handler.d.ts.map +1 -0
  95. package/dist/core/env-handler.js +272 -0
  96. package/dist/core/env-handler.js.map +1 -0
  97. package/dist/core/git-sync.d.ts +88 -0
  98. package/dist/core/git-sync.d.ts.map +1 -0
  99. package/dist/core/git-sync.js +143 -0
  100. package/dist/core/git-sync.js.map +1 -0
  101. package/dist/core/key-store.d.ts +51 -0
  102. package/dist/core/key-store.d.ts.map +1 -0
  103. package/dist/core/key-store.js +108 -0
  104. package/dist/core/key-store.js.map +1 -0
  105. package/dist/core/log-sanitizer.d.ts +72 -0
  106. package/dist/core/log-sanitizer.d.ts.map +1 -0
  107. package/dist/core/log-sanitizer.js +202 -0
  108. package/dist/core/log-sanitizer.js.map +1 -0
  109. package/dist/core/path-validator.d.ts +37 -0
  110. package/dist/core/path-validator.d.ts.map +1 -0
  111. package/dist/core/path-validator.js +127 -0
  112. package/dist/core/path-validator.js.map +1 -0
  113. package/dist/core/recipients.d.ts +99 -0
  114. package/dist/core/recipients.d.ts.map +1 -0
  115. package/dist/core/recipients.js +206 -0
  116. package/dist/core/recipients.js.map +1 -0
  117. package/dist/core/services-handler.d.ts +113 -0
  118. package/dist/core/services-handler.d.ts.map +1 -0
  119. package/dist/core/services-handler.js +176 -0
  120. package/dist/core/services-handler.js.map +1 -0
  121. package/dist/core/state-manager.d.ts +96 -0
  122. package/dist/core/state-manager.d.ts.map +1 -0
  123. package/dist/core/state-manager.js +165 -0
  124. package/dist/core/state-manager.js.map +1 -0
  125. package/dist/core/transport.d.ts +28 -0
  126. package/dist/core/transport.d.ts.map +1 -0
  127. package/dist/core/transport.js +79 -0
  128. package/dist/core/transport.js.map +1 -0
  129. package/dist/index.d.ts +20 -0
  130. package/dist/index.d.ts.map +1 -0
  131. package/dist/index.js +80 -0
  132. package/dist/index.js.map +1 -0
  133. package/dist/types/index.d.ts +5 -0
  134. package/dist/types/index.d.ts.map +1 -0
  135. package/dist/types/index.js +2 -0
  136. package/dist/types/index.js.map +1 -0
  137. package/dist/utils/errors.d.ts +81 -0
  138. package/dist/utils/errors.d.ts.map +1 -0
  139. package/dist/utils/errors.js +191 -0
  140. package/dist/utils/errors.js.map +1 -0
  141. package/dist/utils/secure-memory.d.ts +65 -0
  142. package/dist/utils/secure-memory.d.ts.map +1 -0
  143. package/dist/utils/secure-memory.js +86 -0
  144. package/dist/utils/secure-memory.js.map +1 -0
  145. package/package.json +58 -0
@@ -0,0 +1,176 @@
1
+ /**
2
+ * Services handler module.
3
+ *
4
+ * Manages running-service state: dev servers, background processes,
5
+ * and any port-bound services associated with a project. Services are
6
+ * persisted in `services.age` (encrypted) and restored via the command
7
+ * approval workflow — no command is ever auto-executed.
8
+ *
9
+ * @module core/services-handler
10
+ */
11
+ import { readState, writeState } from './state-manager.js';
12
+ // ─── Helpers ──────────────────────────────────────────────────────────────
13
+ /**
14
+ * Build an empty `ServiceState`.
15
+ */
16
+ function emptyState() {
17
+ return { services: [] };
18
+ }
19
+ // ─── Public API ───────────────────────────────────────────────────────────
20
+ /**
21
+ * Create a new `Service` entry.
22
+ *
23
+ * @param project - The project name this service belongs to.
24
+ * @param name - Human-readable service name (e.g. "api-server").
25
+ * @param port - Port the service listens on.
26
+ * @param command - Shell command to start the service.
27
+ * @param autoStart - Whether the service should be suggested on restore.
28
+ * @returns A typed `Service` object.
29
+ */
30
+ export function createService(project, name, port, command, autoStart = false) {
31
+ return { project, name, port, command, autoStart };
32
+ }
33
+ /**
34
+ * Validate a service entry.
35
+ *
36
+ * Checks:
37
+ * - name is non-empty
38
+ * - port is a positive integer in range 1–65535
39
+ * - command is non-empty
40
+ *
41
+ * @returns An array of human-readable error strings (empty = valid).
42
+ */
43
+ export function validateService(service) {
44
+ const errors = [];
45
+ if (!service.name || service.name.trim().length === 0) {
46
+ errors.push('Service name cannot be empty.');
47
+ }
48
+ if (!service.command || service.command.trim().length === 0) {
49
+ errors.push('Service command cannot be empty.');
50
+ }
51
+ if (!Number.isInteger(service.port) ||
52
+ service.port < 1 ||
53
+ service.port > 65535) {
54
+ errors.push(`Port must be an integer between 1 and 65535, got ${String(service.port)}.`);
55
+ }
56
+ return errors;
57
+ }
58
+ /**
59
+ * Load all services from encrypted state.
60
+ *
61
+ * @param syncDir - The sync directory (e.g. ~/.context-sync).
62
+ * @param privateKey - Age private key for decryption.
63
+ * @returns The decrypted `ServiceState`, or an empty state if the file
64
+ * does not exist.
65
+ */
66
+ export async function loadServices(syncDir, privateKey) {
67
+ const state = await readState(syncDir, privateKey, 'services');
68
+ return state ?? emptyState();
69
+ }
70
+ /**
71
+ * Load services for a specific project.
72
+ *
73
+ * @param syncDir - The sync directory.
74
+ * @param privateKey - Age private key.
75
+ * @param project - Project name to filter by.
76
+ * @returns Array of services belonging to the project (may be empty).
77
+ */
78
+ export async function loadProjectServices(syncDir, privateKey, project) {
79
+ const state = await loadServices(syncDir, privateKey);
80
+ return state.services.filter((s) => s.project === project);
81
+ }
82
+ /**
83
+ * Save (overwrite) the entire services state.
84
+ *
85
+ * @param syncDir - The sync directory.
86
+ * @param state - The complete `ServiceState` to persist.
87
+ * @param publicKey - Age public key for encryption.
88
+ */
89
+ export async function saveServices(syncDir, state, publicKey) {
90
+ await writeState(syncDir, state, publicKey, 'services');
91
+ }
92
+ /**
93
+ * Add a service to the encrypted state.
94
+ *
95
+ * If a service with the same project + name already exists, it is
96
+ * replaced (upsert semantics).
97
+ *
98
+ * @param syncDir - The sync directory.
99
+ * @param service - The service to add/replace.
100
+ * @param publicKey - Age public key for encryption.
101
+ * @param privateKey - Age private key for decryption (needed to read existing state).
102
+ */
103
+ export async function addService(syncDir, service, publicKey, privateKey) {
104
+ const state = await loadServices(syncDir, privateKey);
105
+ // Remove any existing entry with the same project + name (upsert)
106
+ state.services = state.services.filter((s) => !(s.project === service.project && s.name === service.name));
107
+ state.services.push(service);
108
+ await saveServices(syncDir, state, publicKey);
109
+ }
110
+ /**
111
+ * Remove a service by project + name.
112
+ *
113
+ * @param syncDir - The sync directory.
114
+ * @param project - Project name.
115
+ * @param name - Service name.
116
+ * @param publicKey - Age public key for encryption.
117
+ * @param privateKey - Age private key for decryption.
118
+ * @returns `true` if a service was removed, `false` if it was not found.
119
+ */
120
+ export async function removeService(syncDir, project, name, publicKey, privateKey) {
121
+ const state = await loadServices(syncDir, privateKey);
122
+ const before = state.services.length;
123
+ state.services = state.services.filter((s) => !(s.project === project && s.name === name));
124
+ if (state.services.length === before) {
125
+ return false;
126
+ }
127
+ await saveServices(syncDir, state, publicKey);
128
+ return true;
129
+ }
130
+ /**
131
+ * Remove all services for a project.
132
+ *
133
+ * @param syncDir - The sync directory.
134
+ * @param project - Project name.
135
+ * @param publicKey - Age public key for encryption.
136
+ * @param privateKey - Age private key for decryption.
137
+ * @returns Number of services removed.
138
+ */
139
+ export async function removeProjectServices(syncDir, project, publicKey, privateKey) {
140
+ const state = await loadServices(syncDir, privateKey);
141
+ const before = state.services.length;
142
+ state.services = state.services.filter((s) => s.project !== project);
143
+ const removed = before - state.services.length;
144
+ if (removed > 0) {
145
+ await saveServices(syncDir, state, publicKey);
146
+ }
147
+ return removed;
148
+ }
149
+ /**
150
+ * List unique project names that have services.
151
+ *
152
+ * @param syncDir - The sync directory.
153
+ * @param privateKey - Age private key.
154
+ * @returns Sorted array of project names.
155
+ */
156
+ export async function listServiceProjects(syncDir, privateKey) {
157
+ const state = await loadServices(syncDir, privateKey);
158
+ const projects = new Set(state.services.map((s) => s.project));
159
+ return [...projects].sort();
160
+ }
161
+ /**
162
+ * Get services that are marked as auto-start for a project.
163
+ *
164
+ * These are the services that `ctx-sync restore` or
165
+ * `ctx-sync service start` should suggest starting.
166
+ *
167
+ * @param syncDir - The sync directory.
168
+ * @param privateKey - Age private key.
169
+ * @param project - Project name.
170
+ * @returns Services marked with `autoStart: true`.
171
+ */
172
+ export async function getAutoStartServices(syncDir, privateKey, project) {
173
+ const services = await loadProjectServices(syncDir, privateKey, project);
174
+ return services.filter((s) => s.autoStart);
175
+ }
176
+ //# sourceMappingURL=services-handler.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"services-handler.js","sourceRoot":"","sources":["../../src/core/services-handler.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,96 @@
1
+ /**
2
+ * State manager module.
3
+ *
4
+ * Provides read/write operations for all encrypted state files (.age)
5
+ * and the plaintext manifest. State is always encrypted before writing
6
+ * to disk — no plaintext JSON is ever written (except manifest.json
7
+ * which contains only version and timestamps).
8
+ *
9
+ * @module core/state-manager
10
+ */
11
+ import type { StateFile, EnvVars, DockerState, MentalContext, ServiceState, DirectoryState, Manifest } from '@ctx-sync/shared';
12
+ /**
13
+ * Union of all encrypted state data types.
14
+ */
15
+ export type StateData = StateFile | EnvVars | DockerState | MentalContext | ServiceState | DirectoryState;
16
+ /**
17
+ * Map of state file type to filename constant.
18
+ */
19
+ export declare const STATE_FILE_MAP: {
20
+ readonly state: "state.age";
21
+ readonly 'env-vars': "env-vars.age";
22
+ readonly 'docker-state': "docker-state.age";
23
+ readonly 'mental-context': "mental-context.age";
24
+ readonly services: "services.age";
25
+ readonly directories: "directories.age";
26
+ };
27
+ /** Valid state file types */
28
+ export type StateFileType = keyof typeof STATE_FILE_MAP;
29
+ /**
30
+ * Read and decrypt an encrypted state file.
31
+ *
32
+ * Reads the specified `.age` file from the sync directory, decrypts it
33
+ * using the provided private key, and returns the parsed typed data.
34
+ *
35
+ * @param stateDir - The sync directory path (e.g. ~/.context-sync).
36
+ * @param privateKey - The Age private key for decryption.
37
+ * @param fileType - The type of state file to read.
38
+ * @returns The decrypted and parsed state data, or `null` if the file does not exist.
39
+ * @throws If decryption fails (wrong key, corrupted file, etc.).
40
+ */
41
+ export declare function readState<T = StateData>(stateDir: string, privateKey: string, fileType: StateFileType): Promise<T | null>;
42
+ /**
43
+ * Encrypt and write state data to disk.
44
+ *
45
+ * Serialises the data as JSON in memory, encrypts it with Age, and writes
46
+ * the resulting `.age` file. **Never writes plaintext JSON to disk.**
47
+ *
48
+ * Supports both single-recipient and multi-recipient encryption. When
49
+ * `publicKey` is a single string, encrypts for one recipient. When
50
+ * it is an array, encrypts for all recipients (team support).
51
+ *
52
+ * Also updates the manifest to record the file's modification time.
53
+ *
54
+ * @param stateDir - The sync directory path.
55
+ * @param data - The state data to encrypt and write.
56
+ * @param publicKey - A single Age public key or an array of public keys.
57
+ * @param fileType - The type of state file to write.
58
+ * @throws If the filename ends in `.json` (safety check against plaintext writes).
59
+ */
60
+ export declare function writeState(stateDir: string, data: StateData, publicKey: string | string[], fileType: StateFileType): Promise<void>;
61
+ /**
62
+ * Read the plaintext manifest.json from the sync directory.
63
+ *
64
+ * The manifest contains only version and timestamps — no sensitive data.
65
+ * If the file does not exist, returns `null`.
66
+ *
67
+ * @param stateDir - The sync directory path.
68
+ * @returns The parsed manifest, or `null` if it does not exist.
69
+ */
70
+ export declare function readManifest(stateDir: string): Manifest | null;
71
+ /**
72
+ * Write the plaintext manifest.json to the sync directory.
73
+ *
74
+ * The manifest is the only plaintext file in the sync repo.
75
+ * It contains only version and timestamps — no sensitive data.
76
+ *
77
+ * @param stateDir - The sync directory path.
78
+ * @param data - The manifest data to write.
79
+ */
80
+ export declare function writeManifest(stateDir: string, data: Manifest): void;
81
+ /**
82
+ * List all encrypted state files present in the sync directory.
83
+ *
84
+ * @param stateDir - The sync directory path.
85
+ * @returns Array of filenames that exist on disk.
86
+ */
87
+ export declare function listStateFiles(stateDir: string): string[];
88
+ /**
89
+ * Check if a specific state file exists in the sync directory.
90
+ *
91
+ * @param stateDir - The sync directory path.
92
+ * @param fileType - The type of state file to check.
93
+ * @returns `true` if the file exists.
94
+ */
95
+ export declare function stateFileExists(stateDir: string, fileType: StateFileType): boolean;
96
+ //# sourceMappingURL=state-manager.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"state-manager.d.ts","sourceRoot":"","sources":["../../src/core/state-manager.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAKH,OAAO,KAAK,EACV,SAAS,EACT,OAAO,EACP,WAAW,EACX,aAAa,EACb,YAAY,EACZ,cAAc,EACd,QAAQ,EACT,MAAM,kBAAkB,CAAC;AAG1B;;GAEG;AACH,MAAM,MAAM,SAAS,GACjB,SAAS,GACT,OAAO,GACP,WAAW,GACX,aAAa,GACb,YAAY,GACZ,cAAc,CAAC;AAEnB;;GAEG;AACH,eAAO,MAAM,cAAc;;;;;;;CAOjB,CAAC;AAEX,6BAA6B;AAC7B,MAAM,MAAM,aAAa,GAAG,MAAM,OAAO,cAAc,CAAC;AAExD;;;;;;;;;;;GAWG;AACH,wBAAsB,SAAS,CAAC,CAAC,GAAG,SAAS,EAC3C,QAAQ,EAAE,MAAM,EAChB,UAAU,EAAE,MAAM,EAClB,QAAQ,EAAE,aAAa,GACtB,OAAO,CAAC,CAAC,GAAG,IAAI,CAAC,CAenB;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAsB,UAAU,CAC9B,QAAQ,EAAE,MAAM,EAChB,IAAI,EAAE,SAAS,EACf,SAAS,EAAE,MAAM,GAAG,MAAM,EAAE,EAC5B,QAAQ,EAAE,aAAa,GACtB,OAAO,CAAC,IAAI,CAAC,CAwBf;AAED;;;;;;;;GAQG;AACH,wBAAgB,YAAY,CAAC,QAAQ,EAAE,MAAM,GAAG,QAAQ,GAAG,IAAI,CAc9D;AAED;;;;;;;;GAQG;AACH,wBAAgB,aAAa,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,QAAQ,GAAG,IAAI,CAMpE;AAyBD;;;;;GAKG;AACH,wBAAgB,cAAc,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,EAAE,CAOzD;AAED;;;;;;GAMG;AACH,wBAAgB,eAAe,CAAC,QAAQ,EAAE,MAAM,EAAE,QAAQ,EAAE,aAAa,GAAG,OAAO,CAGlF"}
@@ -0,0 +1,165 @@
1
+ /**
2
+ * State manager module.
3
+ *
4
+ * Provides read/write operations for all encrypted state files (.age)
5
+ * and the plaintext manifest. State is always encrypted before writing
6
+ * to disk — no plaintext JSON is ever written (except manifest.json
7
+ * which contains only version and timestamps).
8
+ *
9
+ * @module core/state-manager
10
+ */
11
+ import * as fs from 'node:fs';
12
+ import * as path from 'node:path';
13
+ import { STATE_FILES, VERSION } from '@ctx-sync/shared';
14
+ import { encryptState, encryptStateForRecipients, decryptState } from './encryption.js';
15
+ /**
16
+ * Map of state file type to filename constant.
17
+ */
18
+ export const STATE_FILE_MAP = {
19
+ state: STATE_FILES.STATE,
20
+ 'env-vars': STATE_FILES.ENV_VARS,
21
+ 'docker-state': STATE_FILES.DOCKER_STATE,
22
+ 'mental-context': STATE_FILES.MENTAL_CONTEXT,
23
+ services: STATE_FILES.SERVICES,
24
+ directories: STATE_FILES.DIRECTORIES,
25
+ };
26
+ /**
27
+ * Read and decrypt an encrypted state file.
28
+ *
29
+ * Reads the specified `.age` file from the sync directory, decrypts it
30
+ * using the provided private key, and returns the parsed typed data.
31
+ *
32
+ * @param stateDir - The sync directory path (e.g. ~/.context-sync).
33
+ * @param privateKey - The Age private key for decryption.
34
+ * @param fileType - The type of state file to read.
35
+ * @returns The decrypted and parsed state data, or `null` if the file does not exist.
36
+ * @throws If decryption fails (wrong key, corrupted file, etc.).
37
+ */
38
+ export async function readState(stateDir, privateKey, fileType) {
39
+ const filename = STATE_FILE_MAP[fileType];
40
+ const filePath = path.join(stateDir, filename);
41
+ if (!fs.existsSync(filePath)) {
42
+ return null;
43
+ }
44
+ const ciphertext = fs.readFileSync(filePath, 'utf-8');
45
+ if (!ciphertext.trim()) {
46
+ return null;
47
+ }
48
+ return decryptState(ciphertext, privateKey);
49
+ }
50
+ /**
51
+ * Encrypt and write state data to disk.
52
+ *
53
+ * Serialises the data as JSON in memory, encrypts it with Age, and writes
54
+ * the resulting `.age` file. **Never writes plaintext JSON to disk.**
55
+ *
56
+ * Supports both single-recipient and multi-recipient encryption. When
57
+ * `publicKey` is a single string, encrypts for one recipient. When
58
+ * it is an array, encrypts for all recipients (team support).
59
+ *
60
+ * Also updates the manifest to record the file's modification time.
61
+ *
62
+ * @param stateDir - The sync directory path.
63
+ * @param data - The state data to encrypt and write.
64
+ * @param publicKey - A single Age public key or an array of public keys.
65
+ * @param fileType - The type of state file to write.
66
+ * @throws If the filename ends in `.json` (safety check against plaintext writes).
67
+ */
68
+ export async function writeState(stateDir, data, publicKey, fileType) {
69
+ const filename = STATE_FILE_MAP[fileType];
70
+ // Safety check: never write plaintext JSON state files
71
+ if (filename.endsWith('.json')) {
72
+ throw new Error(`Cannot write unencrypted state file: ${filename}. ` +
73
+ 'State files must be encrypted as .age files.');
74
+ }
75
+ // Ensure the directory exists
76
+ fs.mkdirSync(stateDir, { recursive: true });
77
+ // Encrypt for single or multiple recipients
78
+ const ciphertext = Array.isArray(publicKey)
79
+ ? await encryptStateForRecipients(data, publicKey)
80
+ : await encryptState(data, publicKey);
81
+ const filePath = path.join(stateDir, filename);
82
+ fs.writeFileSync(filePath, ciphertext, 'utf-8');
83
+ // Update manifest with new modification timestamp
84
+ updateManifestEntry(stateDir, filename);
85
+ }
86
+ /**
87
+ * Read the plaintext manifest.json from the sync directory.
88
+ *
89
+ * The manifest contains only version and timestamps — no sensitive data.
90
+ * If the file does not exist, returns `null`.
91
+ *
92
+ * @param stateDir - The sync directory path.
93
+ * @returns The parsed manifest, or `null` if it does not exist.
94
+ */
95
+ export function readManifest(stateDir) {
96
+ const filePath = path.join(stateDir, STATE_FILES.MANIFEST);
97
+ if (!fs.existsSync(filePath)) {
98
+ return null;
99
+ }
100
+ const content = fs.readFileSync(filePath, 'utf-8');
101
+ if (!content.trim()) {
102
+ return null;
103
+ }
104
+ return JSON.parse(content);
105
+ }
106
+ /**
107
+ * Write the plaintext manifest.json to the sync directory.
108
+ *
109
+ * The manifest is the only plaintext file in the sync repo.
110
+ * It contains only version and timestamps — no sensitive data.
111
+ *
112
+ * @param stateDir - The sync directory path.
113
+ * @param data - The manifest data to write.
114
+ */
115
+ export function writeManifest(stateDir, data) {
116
+ // Ensure the directory exists
117
+ fs.mkdirSync(stateDir, { recursive: true });
118
+ const filePath = path.join(stateDir, STATE_FILES.MANIFEST);
119
+ fs.writeFileSync(filePath, JSON.stringify(data, null, 2), 'utf-8');
120
+ }
121
+ /**
122
+ * Update a single entry in the manifest with a new modification timestamp.
123
+ *
124
+ * If the manifest does not exist, a new one is created.
125
+ *
126
+ * @param stateDir - The sync directory path.
127
+ * @param filename - The state file name (e.g. 'state.age').
128
+ */
129
+ function updateManifestEntry(stateDir, filename) {
130
+ const manifest = readManifest(stateDir) ?? {
131
+ version: VERSION,
132
+ lastSync: new Date().toISOString(),
133
+ files: {},
134
+ };
135
+ manifest.files[filename] = {
136
+ lastModified: new Date().toISOString(),
137
+ };
138
+ manifest.lastSync = new Date().toISOString();
139
+ writeManifest(stateDir, manifest);
140
+ }
141
+ /**
142
+ * List all encrypted state files present in the sync directory.
143
+ *
144
+ * @param stateDir - The sync directory path.
145
+ * @returns Array of filenames that exist on disk.
146
+ */
147
+ export function listStateFiles(stateDir) {
148
+ if (!fs.existsSync(stateDir)) {
149
+ return [];
150
+ }
151
+ const entries = fs.readdirSync(stateDir);
152
+ return entries.filter((entry) => entry.endsWith('.age'));
153
+ }
154
+ /**
155
+ * Check if a specific state file exists in the sync directory.
156
+ *
157
+ * @param stateDir - The sync directory path.
158
+ * @param fileType - The type of state file to check.
159
+ * @returns `true` if the file exists.
160
+ */
161
+ export function stateFileExists(stateDir, fileType) {
162
+ const filename = STATE_FILE_MAP[fileType];
163
+ return fs.existsSync(path.join(stateDir, filename));
164
+ }
165
+ //# sourceMappingURL=state-manager.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"state-manager.js","sourceRoot":"","sources":["../../src/core/state-manager.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,KAAK,EAAE,MAAM,SAAS,CAAC;AAC9B,OAAO,KAAK,IAAI,MAAM,WAAW,CAAC;AAClC,OAAO,EAAE,WAAW,EAAE,OAAO,EAAE,MAAM,kBAAkB,CAAC;AAUxD,OAAO,EAAE,YAAY,EAAE,yBAAyB,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC;AAaxF;;GAEG;AACH,MAAM,CAAC,MAAM,cAAc,GAAG;IAC5B,KAAK,EAAE,WAAW,CAAC,KAAK;IACxB,UAAU,EAAE,WAAW,CAAC,QAAQ;IAChC,cAAc,EAAE,WAAW,CAAC,YAAY;IACxC,gBAAgB,EAAE,WAAW,CAAC,cAAc;IAC5C,QAAQ,EAAE,WAAW,CAAC,QAAQ;IAC9B,WAAW,EAAE,WAAW,CAAC,WAAW;CAC5B,CAAC;AAKX;;;;;;;;;;;GAWG;AACH,MAAM,CAAC,KAAK,UAAU,SAAS,CAC7B,QAAgB,EAChB,UAAkB,EAClB,QAAuB;IAEvB,MAAM,QAAQ,GAAG,cAAc,CAAC,QAAQ,CAAC,CAAC;IAC1C,MAAM,QAAQ,GAAG,IAAI,CAAC,IAAI,CAAC,QAAQ,EAAE,QAAQ,CAAC,CAAC;IAE/C,IAAI,CAAC,EAAE,CAAC,UAAU,CAAC,QAAQ,CAAC,EAAE,CAAC;QAC7B,OAAO,IAAI,CAAC;IACd,CAAC;IAED,MAAM,UAAU,GAAG,EAAE,CAAC,YAAY,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAC;IAEtD,IAAI,CAAC,UAAU,CAAC,IAAI,EAAE,EAAE,CAAC;QACvB,OAAO,IAAI,CAAC;IACd,CAAC;IAED,OAAO,YAAY,CAAI,UAAU,EAAE,UAAU,CAAC,CAAC;AACjD,CAAC;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,MAAM,CAAC,KAAK,UAAU,UAAU,CAC9B,QAAgB,EAChB,IAAe,EACf,SAA4B,EAC5B,QAAuB;IAEvB,MAAM,QAAQ,GAAG,cAAc,CAAC,QAAQ,CAAC,CAAC;IAE1C,uDAAuD;IACvD,IAAI,QAAQ,CAAC,QAAQ,CAAC,OAAO,CAAC,EAAE,CAAC;QAC/B,MAAM,IAAI,KAAK,CACb,wCAAwC,QAAQ,IAAI;YAClD,8CAA8C,CACjD,CAAC;IACJ,CAAC;IAED,8BAA8B;IAC9B,EAAE,CAAC,SAAS,CAAC,QAAQ,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IAE5C,4CAA4C;IAC5C,MAAM,UAAU,GAAG,KAAK,CAAC,OAAO,CAAC,SAAS,CAAC;QACzC,CAAC,CAAC,MAAM,yBAAyB,CAAC,IAAI,EAAE,SAAS,CAAC;QAClD,CAAC,CAAC,MAAM,YAAY,CAAC,IAAI,EAAE,SAAS,CAAC,CAAC;IAExC,MAAM,QAAQ,GAAG,IAAI,CAAC,IAAI,CAAC,QAAQ,EAAE,QAAQ,CAAC,CAAC;IAC/C,EAAE,CAAC,aAAa,CAAC,QAAQ,EAAE,UAAU,EAAE,OAAO,CAAC,CAAC;IAEhD,kDAAkD;IAClD,mBAAmB,CAAC,QAAQ,EAAE,QAAQ,CAAC,CAAC;AAC1C,CAAC;AAED;;;;;;;;GAQG;AACH,MAAM,UAAU,YAAY,CAAC,QAAgB;IAC3C,MAAM,QAAQ,GAAG,IAAI,CAAC,IAAI,CAAC,QAAQ,EAAE,WAAW,CAAC,QAAQ,CAAC,CAAC;IAE3D,IAAI,CAAC,EAAE,CAAC,UAAU,CAAC,QAAQ,CAAC,EAAE,CAAC;QAC7B,OAAO,IAAI,CAAC;IACd,CAAC;IAED,MAAM,OAAO,GAAG,EAAE,CAAC,YAAY,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAC;IAEnD,IAAI,CAAC,OAAO,CAAC,IAAI,EAAE,EAAE,CAAC;QACpB,OAAO,IAAI,CAAC;IACd,CAAC;IAED,OAAO,IAAI,CAAC,KAAK,CAAC,OAAO,CAAa,CAAC;AACzC,CAAC;AAED;;;;;;;;GAQG;AACH,MAAM,UAAU,aAAa,CAAC,QAAgB,EAAE,IAAc;IAC5D,8BAA8B;IAC9B,EAAE,CAAC,SAAS,CAAC,QAAQ,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IAE5C,MAAM,QAAQ,GAAG,IAAI,CAAC,IAAI,CAAC,QAAQ,EAAE,WAAW,CAAC,QAAQ,CAAC,CAAC;IAC3D,EAAE,CAAC,aAAa,CAAC,QAAQ,EAAE,IAAI,CAAC,SAAS,CAAC,IAAI,EAAE,IAAI,EAAE,CAAC,CAAC,EAAE,OAAO,CAAC,CAAC;AACrE,CAAC;AAED;;;;;;;GAOG;AACH,SAAS,mBAAmB,CAAC,QAAgB,EAAE,QAAgB;IAC7D,MAAM,QAAQ,GAAG,YAAY,CAAC,QAAQ,CAAC,IAAI;QACzC,OAAO,EAAE,OAAO;QAChB,QAAQ,EAAE,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE;QAClC,KAAK,EAAE,EAAE;KACV,CAAC;IAEF,QAAQ,CAAC,KAAK,CAAC,QAAQ,CAAC,GAAG;QACzB,YAAY,EAAE,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE;KACvC,CAAC;IACF,QAAQ,CAAC,QAAQ,GAAG,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC;IAE7C,aAAa,CAAC,QAAQ,EAAE,QAAQ,CAAC,CAAC;AACpC,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,cAAc,CAAC,QAAgB;IAC7C,IAAI,CAAC,EAAE,CAAC,UAAU,CAAC,QAAQ,CAAC,EAAE,CAAC;QAC7B,OAAO,EAAE,CAAC;IACZ,CAAC;IAED,MAAM,OAAO,GAAG,EAAE,CAAC,WAAW,CAAC,QAAQ,CAAC,CAAC;IACzC,OAAO,OAAO,CAAC,MAAM,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC;AAC3D,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,eAAe,CAAC,QAAgB,EAAE,QAAuB;IACvE,MAAM,QAAQ,GAAG,cAAc,CAAC,QAAQ,CAAC,CAAC;IAC1C,OAAO,EAAE,CAAC,UAAU,CAAC,IAAI,CAAC,IAAI,CAAC,QAAQ,EAAE,QAAQ,CAAC,CAAC,CAAC;AACtD,CAAC"}
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Transport security validation module.
3
+ *
4
+ * Validates Git remote URLs to ensure only secure transport protocols
5
+ * (SSH and HTTPS) are used. Rejects insecure protocols such as HTTP,
6
+ * git://, and ftp:// to prevent data exposure in transit.
7
+ *
8
+ * @module core/transport
9
+ */
10
+ /**
11
+ * Validate that a Git remote URL uses a secure transport protocol.
12
+ *
13
+ * Accepts:
14
+ * - SSH URLs: `git@github.com:user/repo.git`
15
+ * - HTTPS URLs: `https://github.com/user/repo.git`
16
+ * - Local paths: `/path/to/repo.git` or `file:///path/to/repo.git`
17
+ *
18
+ * Rejects:
19
+ * - HTTP URLs: `http://...`
20
+ * - Git protocol: `git://...`
21
+ * - FTP protocol: `ftp://...`
22
+ * - Empty or malformed URLs
23
+ *
24
+ * @param url - The Git remote URL to validate.
25
+ * @throws If the URL is empty, malformed, or uses an insecure protocol.
26
+ */
27
+ export declare function validateRemoteUrl(url: string): void;
28
+ //# sourceMappingURL=transport.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"transport.d.ts","sourceRoot":"","sources":["../../src/core/transport.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AASH;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAoEnD"}
@@ -0,0 +1,79 @@
1
+ /**
2
+ * Transport security validation module.
3
+ *
4
+ * Validates Git remote URLs to ensure only secure transport protocols
5
+ * (SSH and HTTPS) are used. Rejects insecure protocols such as HTTP,
6
+ * git://, and ftp:// to prevent data exposure in transit.
7
+ *
8
+ * @module core/transport
9
+ */
10
+ /** Allowed transport protocols for Git remotes.
11
+ * file: is permitted because local-path remotes have no network transit. */
12
+ const ALLOWED_PROTOCOLS = ['https:', 'ssh:', 'file:'];
13
+ /** SSH remote URL pattern: git@host:user/repo.git */
14
+ const SSH_PATTERN = /^[a-zA-Z0-9._-]+@[a-zA-Z0-9._-]+:.+$/;
15
+ /**
16
+ * Validate that a Git remote URL uses a secure transport protocol.
17
+ *
18
+ * Accepts:
19
+ * - SSH URLs: `git@github.com:user/repo.git`
20
+ * - HTTPS URLs: `https://github.com/user/repo.git`
21
+ * - Local paths: `/path/to/repo.git` or `file:///path/to/repo.git`
22
+ *
23
+ * Rejects:
24
+ * - HTTP URLs: `http://...`
25
+ * - Git protocol: `git://...`
26
+ * - FTP protocol: `ftp://...`
27
+ * - Empty or malformed URLs
28
+ *
29
+ * @param url - The Git remote URL to validate.
30
+ * @throws If the URL is empty, malformed, or uses an insecure protocol.
31
+ */
32
+ export function validateRemoteUrl(url) {
33
+ if (!url || typeof url !== 'string') {
34
+ throw new Error('Git remote URL is required.\n' +
35
+ 'Provide an SSH (git@host:user/repo.git) or HTTPS (https://host/user/repo.git) URL.');
36
+ }
37
+ const trimmed = url.trim();
38
+ if (trimmed.length === 0) {
39
+ throw new Error('Git remote URL is required.\n' +
40
+ 'Provide an SSH (git@host:user/repo.git) or HTTPS (https://host/user/repo.git) URL.');
41
+ }
42
+ // Check for SSH-style URLs first (git@host:user/repo.git)
43
+ if (SSH_PATTERN.test(trimmed)) {
44
+ return; // SSH URLs are always secure
45
+ }
46
+ // Allow absolute filesystem paths — no network transit, no security concern
47
+ if (trimmed.startsWith('/')) {
48
+ return;
49
+ }
50
+ // Try to parse as a URL with a protocol
51
+ let parsed;
52
+ try {
53
+ parsed = new URL(trimmed);
54
+ }
55
+ catch {
56
+ throw new Error(`Invalid Git remote URL: ${trimmed}\n` +
57
+ 'Expected SSH (git@host:user/repo.git) or HTTPS (https://host/user/repo.git) URL.');
58
+ }
59
+ // Check for insecure protocols with specific error messages
60
+ if (parsed.protocol === 'http:') {
61
+ throw new Error(`Insecure Git remote: ${trimmed}\n` +
62
+ 'HTTP transmits data in plaintext. Use HTTPS instead:\n' +
63
+ ` ${trimmed.replace(/^http:/, 'https:')}`);
64
+ }
65
+ if (parsed.protocol === 'git:') {
66
+ throw new Error(`Insecure Git remote: ${trimmed}\n` +
67
+ 'The git:// protocol transmits data in plaintext. Use SSH or HTTPS instead.');
68
+ }
69
+ if (parsed.protocol === 'ftp:') {
70
+ throw new Error(`Insecure Git remote: ${trimmed}\n` +
71
+ 'FTP transmits data in plaintext. Use SSH or HTTPS instead.');
72
+ }
73
+ // Check against allowed protocols
74
+ if (!ALLOWED_PROTOCOLS.includes(parsed.protocol)) {
75
+ throw new Error(`Unsupported Git remote protocol: ${parsed.protocol}\n` +
76
+ 'Only SSH and HTTPS are supported.');
77
+ }
78
+ }
79
+ //# sourceMappingURL=transport.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"transport.js","sourceRoot":"","sources":["../../src/core/transport.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH;6EAC6E;AAC7E,MAAM,iBAAiB,GAAG,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,CAAU,CAAC;AAE/D,qDAAqD;AACrD,MAAM,WAAW,GAAG,sCAAsC,CAAC;AAE3D;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,UAAU,iBAAiB,CAAC,GAAW;IAC3C,IAAI,CAAC,GAAG,IAAI,OAAO,GAAG,KAAK,QAAQ,EAAE,CAAC;QACpC,MAAM,IAAI,KAAK,CACb,+BAA+B;YAC7B,oFAAoF,CACvF,CAAC;IACJ,CAAC;IAED,MAAM,OAAO,GAAG,GAAG,CAAC,IAAI,EAAE,CAAC;IAE3B,IAAI,OAAO,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACzB,MAAM,IAAI,KAAK,CACb,+BAA+B;YAC7B,oFAAoF,CACvF,CAAC;IACJ,CAAC;IAED,0DAA0D;IAC1D,IAAI,WAAW,CAAC,IAAI,CAAC,OAAO,CAAC,EAAE,CAAC;QAC9B,OAAO,CAAC,6BAA6B;IACvC,CAAC;IAED,4EAA4E;IAC5E,IAAI,OAAO,CAAC,UAAU,CAAC,GAAG,CAAC,EAAE,CAAC;QAC5B,OAAO;IACT,CAAC;IAED,wCAAwC;IACxC,IAAI,MAAW,CAAC;IAChB,IAAI,CAAC;QACH,MAAM,GAAG,IAAI,GAAG,CAAC,OAAO,CAAC,CAAC;IAC5B,CAAC;IAAC,MAAM,CAAC;QACP,MAAM,IAAI,KAAK,CACb,2BAA2B,OAAO,IAAI;YACpC,kFAAkF,CACrF,CAAC;IACJ,CAAC;IAED,4DAA4D;IAC5D,IAAI,MAAM,CAAC,QAAQ,KAAK,OAAO,EAAE,CAAC;QAChC,MAAM,IAAI,KAAK,CACb,wBAAwB,OAAO,IAAI;YACjC,wDAAwD;YACxD,KAAK,OAAO,CAAC,OAAO,CAAC,QAAQ,EAAE,QAAQ,CAAC,EAAE,CAC7C,CAAC;IACJ,CAAC;IAED,IAAI,MAAM,CAAC,QAAQ,KAAK,MAAM,EAAE,CAAC;QAC/B,MAAM,IAAI,KAAK,CACb,wBAAwB,OAAO,IAAI;YACjC,4EAA4E,CAC/E,CAAC;IACJ,CAAC;IAED,IAAI,MAAM,CAAC,QAAQ,KAAK,MAAM,EAAE,CAAC;QAC/B,MAAM,IAAI,KAAK,CACb,wBAAwB,OAAO,IAAI;YACjC,4DAA4D,CAC/D,CAAC;IACJ,CAAC;IAED,kCAAkC;IAClC,IAAI,CAAC,iBAAiB,CAAC,QAAQ,CAAC,MAAM,CAAC,QAA8C,CAAC,EAAE,CAAC;QACvF,MAAM,IAAI,KAAK,CACb,oCAAoC,MAAM,CAAC,QAAQ,IAAI;YACrD,mCAAmC,CACtC,CAAC;IACJ,CAAC;AACH,CAAC"}
@@ -0,0 +1,20 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * ctx-sync — Sync your complete development context across machines.
4
+ *
5
+ * CLI entry point. Uses Commander.js for command parsing and routing.
6
+ *
7
+ * @module ctx-sync
8
+ */
9
+ import { Command } from 'commander';
10
+ /**
11
+ * Create and configure the root CLI program.
12
+ *
13
+ * @returns The configured Commander program instance.
14
+ */
15
+ export declare function createProgram(): Command;
16
+ /**
17
+ * Main entry point — parse CLI arguments and execute the matched command.
18
+ */
19
+ export declare function main(argv?: string[]): Promise<void>;
20
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AAEA;;;;;;GAMG;AAEH,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAsBpC;;;;GAIG;AACH,wBAAgB,aAAa,IAAI,OAAO,CA6BvC;AAED;;GAEG;AACH,wBAAsB,IAAI,CAAC,IAAI,CAAC,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAGzD"}