@tinyrack/devsync 1.1.0 → 1.3.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 (220) hide show
  1. package/README.md +230 -62
  2. package/dist/cli/base-command.d.ts +14 -0
  3. package/dist/cli/base-command.d.ts.map +1 -0
  4. package/dist/cli/base-command.js +22 -0
  5. package/dist/cli/base-command.js.map +1 -0
  6. package/dist/cli/commands/dir.d.ts +8 -0
  7. package/dist/cli/commands/dir.d.ts.map +1 -0
  8. package/dist/cli/commands/dir.js +16 -0
  9. package/dist/cli/commands/dir.js.map +1 -0
  10. package/dist/cli/commands/doctor.d.ts +8 -0
  11. package/dist/cli/commands/doctor.d.ts.map +1 -0
  12. package/dist/cli/commands/doctor.js +18 -0
  13. package/dist/cli/commands/doctor.js.map +1 -0
  14. package/dist/cli/commands/index.d.ts +23 -0
  15. package/dist/cli/commands/index.d.ts.map +1 -0
  16. package/dist/cli/commands/index.js +23 -0
  17. package/dist/cli/commands/index.js.map +1 -0
  18. package/dist/cli/commands/init.d.ts +15 -0
  19. package/dist/cli/commands/init.d.ts.map +1 -0
  20. package/dist/cli/commands/init.js +43 -0
  21. package/dist/cli/commands/init.js.map +1 -0
  22. package/dist/cli/commands/machine/list.d.ts +7 -0
  23. package/dist/cli/commands/machine/list.d.ts.map +1 -0
  24. package/dist/cli/commands/machine/list.js +12 -0
  25. package/dist/cli/commands/machine/list.js.map +1 -0
  26. package/dist/cli/commands/machine/use.d.ts +11 -0
  27. package/dist/cli/commands/machine/use.d.ts.map +1 -0
  28. package/dist/cli/commands/machine/use.js +28 -0
  29. package/dist/cli/commands/machine/use.js.map +1 -0
  30. package/dist/cli/commands/pull.d.ts +12 -0
  31. package/dist/cli/commands/pull.d.ts.map +1 -0
  32. package/dist/cli/commands/pull.js +34 -0
  33. package/dist/cli/commands/pull.js.map +1 -0
  34. package/dist/cli/commands/push.d.ts +12 -0
  35. package/dist/cli/commands/push.d.ts.map +1 -0
  36. package/dist/cli/commands/push.js +34 -0
  37. package/dist/cli/commands/push.js.map +1 -0
  38. package/dist/cli/commands/status.d.ts +11 -0
  39. package/dist/cli/commands/status.d.ts.map +1 -0
  40. package/dist/cli/commands/status.js +27 -0
  41. package/dist/cli/commands/status.js.map +1 -0
  42. package/dist/cli/commands/track.d.ts +16 -0
  43. package/dist/cli/commands/track.d.ts.map +1 -0
  44. package/dist/cli/commands/track.js +82 -0
  45. package/dist/cli/commands/track.js.map +1 -0
  46. package/dist/cli/commands/untrack.d.ts +11 -0
  47. package/dist/cli/commands/untrack.d.ts.map +1 -0
  48. package/dist/cli/commands/untrack.js +28 -0
  49. package/dist/cli/commands/untrack.js.map +1 -0
  50. package/dist/config/global-config.d.ts +21 -0
  51. package/dist/config/global-config.d.ts.map +1 -0
  52. package/dist/config/global-config.js +106 -0
  53. package/dist/config/global-config.js.map +1 -0
  54. package/dist/config/platform.d.ts +11 -0
  55. package/dist/config/platform.d.ts.map +1 -0
  56. package/dist/config/platform.js +19 -0
  57. package/dist/config/platform.js.map +1 -0
  58. package/dist/config/sync.d.ts +107 -0
  59. package/dist/config/sync.d.ts.map +1 -0
  60. package/dist/config/sync.js +424 -0
  61. package/dist/config/sync.js.map +1 -0
  62. package/dist/config/xdg.d.ts +14 -0
  63. package/dist/config/xdg.d.ts.map +1 -0
  64. package/dist/config/xdg.js +102 -0
  65. package/dist/config/xdg.js.map +1 -0
  66. package/dist/index.d.ts +3 -0
  67. package/dist/index.d.ts.map +1 -0
  68. package/{src/index.ts → dist/index.js} +1 -1
  69. package/dist/index.js.map +1 -0
  70. package/dist/lib/file-mode.d.ts +3 -0
  71. package/dist/lib/file-mode.d.ts.map +1 -0
  72. package/dist/lib/file-mode.js +7 -0
  73. package/dist/lib/file-mode.js.map +1 -0
  74. package/dist/lib/output.d.ts +31 -0
  75. package/dist/lib/output.d.ts.map +1 -0
  76. package/dist/lib/output.js +198 -0
  77. package/dist/lib/output.js.map +1 -0
  78. package/dist/lib/path.d.ts +5 -0
  79. package/dist/lib/path.d.ts.map +1 -0
  80. package/dist/lib/path.js +25 -0
  81. package/dist/lib/path.js.map +1 -0
  82. package/dist/lib/string.d.ts +2 -0
  83. package/dist/lib/string.d.ts.map +1 -0
  84. package/dist/lib/string.js +4 -0
  85. package/dist/lib/string.js.map +1 -0
  86. package/dist/lib/validation.d.ts +3 -0
  87. package/dist/lib/validation.d.ts.map +1 -0
  88. package/dist/lib/validation.js +9 -0
  89. package/dist/lib/validation.js.map +1 -0
  90. package/dist/services/add.d.ts +20 -0
  91. package/dist/services/add.d.ts.map +1 -0
  92. package/dist/services/add.js +161 -0
  93. package/dist/services/add.js.map +1 -0
  94. package/dist/services/config-file.d.ts +45 -0
  95. package/dist/services/config-file.d.ts.map +1 -0
  96. package/dist/services/config-file.js +35 -0
  97. package/dist/services/config-file.js.map +1 -0
  98. package/dist/services/crypto.d.ts +9 -0
  99. package/dist/services/crypto.d.ts.map +1 -0
  100. package/dist/services/crypto.js +75 -0
  101. package/dist/services/crypto.js.map +1 -0
  102. package/dist/services/doctor.d.ts +16 -0
  103. package/dist/services/doctor.d.ts.map +1 -0
  104. package/dist/services/doctor.js +84 -0
  105. package/dist/services/doctor.js.map +1 -0
  106. package/dist/services/error.d.ts +14 -0
  107. package/dist/services/error.d.ts.map +1 -0
  108. package/dist/services/error.js +38 -0
  109. package/dist/services/error.js.map +1 -0
  110. package/dist/services/filesystem.d.ts +15 -0
  111. package/dist/services/filesystem.d.ts.map +1 -0
  112. package/dist/services/filesystem.js +113 -0
  113. package/dist/services/filesystem.js.map +1 -0
  114. package/dist/services/forget.d.ts +14 -0
  115. package/dist/services/forget.d.ts.map +1 -0
  116. package/dist/services/forget.js +124 -0
  117. package/dist/services/forget.js.map +1 -0
  118. package/dist/services/git.d.ts +10 -0
  119. package/dist/services/git.d.ts.map +1 -0
  120. package/dist/services/git.js +57 -0
  121. package/dist/services/git.js.map +1 -0
  122. package/dist/services/init.d.ts +19 -0
  123. package/dist/services/init.d.ts.map +1 -0
  124. package/dist/services/init.js +203 -0
  125. package/dist/services/init.js.map +1 -0
  126. package/dist/services/local-materialization.d.ts +28 -0
  127. package/dist/services/local-materialization.d.ts.map +1 -0
  128. package/dist/services/local-materialization.js +262 -0
  129. package/dist/services/local-materialization.js.map +1 -0
  130. package/dist/services/local-snapshot.d.ts +25 -0
  131. package/dist/services/local-snapshot.d.ts.map +1 -0
  132. package/dist/services/local-snapshot.js +93 -0
  133. package/dist/services/local-snapshot.js.map +1 -0
  134. package/dist/services/machine.d.ts +40 -0
  135. package/dist/services/machine.d.ts.map +1 -0
  136. package/dist/services/machine.js +113 -0
  137. package/dist/services/machine.js.map +1 -0
  138. package/dist/services/paths.d.ts +13 -0
  139. package/dist/services/paths.d.ts.map +1 -0
  140. package/dist/services/paths.js +71 -0
  141. package/dist/services/paths.js.map +1 -0
  142. package/dist/services/pull.d.ts +28 -0
  143. package/dist/services/pull.d.ts.map +1 -0
  144. package/dist/services/pull.js +67 -0
  145. package/dist/services/pull.js.map +1 -0
  146. package/dist/services/push.d.ts +35 -0
  147. package/dist/services/push.d.ts.map +1 -0
  148. package/dist/services/push.js +96 -0
  149. package/dist/services/push.js.map +1 -0
  150. package/dist/services/repo-artifacts.d.ts +52 -0
  151. package/dist/services/repo-artifacts.d.ts.map +1 -0
  152. package/dist/services/repo-artifacts.js +251 -0
  153. package/dist/services/repo-artifacts.js.map +1 -0
  154. package/dist/services/repo-snapshot.d.ts +6 -0
  155. package/dist/services/repo-snapshot.d.ts.map +1 -0
  156. package/dist/services/repo-snapshot.js +163 -0
  157. package/dist/services/repo-snapshot.js.map +1 -0
  158. package/dist/services/runtime.d.ts +40 -0
  159. package/dist/services/runtime.d.ts.map +1 -0
  160. package/dist/services/runtime.js +71 -0
  161. package/dist/services/runtime.js.map +1 -0
  162. package/dist/services/set.d.ts +38 -0
  163. package/dist/services/set.d.ts.map +1 -0
  164. package/dist/services/set.js +184 -0
  165. package/dist/services/set.js.map +1 -0
  166. package/dist/services/status.d.ts +30 -0
  167. package/dist/services/status.d.ts.map +1 -0
  168. package/dist/services/status.js +35 -0
  169. package/dist/services/status.js.map +1 -0
  170. package/package.json +15 -7
  171. package/src/cli/commands/add.ts +0 -40
  172. package/src/cli/commands/cd.ts +0 -80
  173. package/src/cli/commands/doctor.ts +0 -20
  174. package/src/cli/commands/forget.ts +0 -32
  175. package/src/cli/commands/index.ts +0 -23
  176. package/src/cli/commands/init.ts +0 -43
  177. package/src/cli/commands/list.ts +0 -17
  178. package/src/cli/commands/pull.ts +0 -31
  179. package/src/cli/commands/push.ts +0 -31
  180. package/src/cli/commands/set.ts +0 -47
  181. package/src/cli/commands/status.ts +0 -18
  182. package/src/cli/sync-output.test.ts +0 -173
  183. package/src/cli/sync-output.ts +0 -200
  184. package/src/config/sync.test.ts +0 -609
  185. package/src/config/sync.ts +0 -572
  186. package/src/config/xdg.ts +0 -138
  187. package/src/lib/string.test.ts +0 -13
  188. package/src/lib/string.ts +0 -3
  189. package/src/lib/validation.test.ts +0 -32
  190. package/src/lib/validation.ts +0 -11
  191. package/src/services/add.ts +0 -178
  192. package/src/services/config-file.test.ts +0 -161
  193. package/src/services/config-file.ts +0 -101
  194. package/src/services/crypto.test.ts +0 -132
  195. package/src/services/crypto.ts +0 -83
  196. package/src/services/doctor.ts +0 -142
  197. package/src/services/error.ts +0 -6
  198. package/src/services/filesystem.test.ts +0 -171
  199. package/src/services/filesystem.ts +0 -183
  200. package/src/services/forget.ts +0 -261
  201. package/src/services/git.test.ts +0 -83
  202. package/src/services/git.ts +0 -74
  203. package/src/services/init.test.ts +0 -109
  204. package/src/services/init.ts +0 -244
  205. package/src/services/list.ts +0 -63
  206. package/src/services/local-materialization.ts +0 -421
  207. package/src/services/local-snapshot.ts +0 -173
  208. package/src/services/paths.test.ts +0 -74
  209. package/src/services/paths.ts +0 -98
  210. package/src/services/pull.ts +0 -144
  211. package/src/services/push.ts +0 -168
  212. package/src/services/repo-artifacts.ts +0 -262
  213. package/src/services/repo-snapshot.ts +0 -197
  214. package/src/services/runtime.ts +0 -57
  215. package/src/services/set.ts +0 -383
  216. package/src/services/status.ts +0 -57
  217. package/src/services/sync.dry-run.test.ts +0 -179
  218. package/src/services/sync.runtime.test.ts +0 -756
  219. package/src/services/sync.service.test.ts +0 -1169
  220. package/src/test/helpers/sync-fixture.ts +0 -47
package/README.md CHANGED
@@ -1,30 +1,48 @@
1
1
  # devsync
2
2
 
3
- A personal CLI tool for git-backed configuration sync.
3
+ `devsync` is a cross-platform CLI for managing the configuration files in your home directory with git and syncing them across multiple machines.
4
4
 
5
- `devsync` is a Node.js + TypeScript command-line utility for managing a synced configuration repository under your XDG config directory. It tracks files and directories under `HOME`, stores plain and encrypted artifacts in a git-backed sync repo, and can push local state into that repo or pull the repo back onto the machine.
5
+ Instead of treating the repository as the source of truth, `devsync` treats your actual local config as the truth. You choose files and directories under `HOME`, `devsync` mirrors them into a git-backed sync repository, and later restores that repository onto another machine when you need it.
6
6
 
7
- ## Features
7
+ ## 1. Purpose and how it differs
8
8
 
9
- - Flat sync-focused CLI: `init`, `add`, `set`, `forget`, `list`, `status`, `doctor`, `push`, `pull`, `cd`
10
- - Git-backed sync repository under `~/.config/devsync/sync`
11
- - Age-encrypted secret file support
12
- - Rule-based `normal`, `secret`, and `ignore` modes
13
- - Direct TypeScript execution with Node.js 24+
14
- - Shell autocomplete via oclif
9
+ Most dotfiles tools start from the repository and ask you to shape your local machine around it.
15
10
 
16
- ## Requirements
11
+ `devsync` takes the opposite approach:
12
+
13
+ - Your real config under `HOME` is the source of truth.
14
+ - The git repository is a sync artifact, not the primary authoring location.
15
+ - `push` captures your current machine state into the repository.
16
+ - `pull` applies the repository back onto another machine.
17
+
18
+ That makes `devsync` a good fit when you want to:
19
+
20
+ - manage existing dotfiles and app configs without reorganizing your home directory,
21
+ - keep machine-specific config workflows intact,
22
+ - sync plain files and encrypted secrets together,
23
+ - use normal git remotes as the transport layer between PCs.
24
+
25
+ Core capabilities:
26
+
27
+ - track files and directories under your home directory,
28
+ - store synced artifacts in `~/.config/devsync/sync`,
29
+ - mark paths as `normal`, `secret`, or `ignore`,
30
+ - encrypt secret artifacts with `age`,
31
+ - preview both directions with `status`, `push --dry-run`, and `pull --dry-run`.
32
+
33
+ ## 2. Installation
34
+
35
+ Requirements:
17
36
 
18
37
  - Node.js 24+
19
38
  - npm
20
39
  - git
21
40
 
22
- ## Installation
23
-
24
41
  Install globally:
25
42
 
26
43
  ```bash
27
44
  npm install -g @tinyrack/devsync
45
+ devsync --help
28
46
  ```
29
47
 
30
48
  Run without installing globally:
@@ -33,64 +51,177 @@ Run without installing globally:
33
51
  npx @tinyrack/devsync --help
34
52
  ```
35
53
 
36
- Run the CLI locally:
54
+ Run from this checkout:
37
55
 
38
56
  ```bash
57
+ npm install
39
58
  npm run start -- --help
40
59
  ```
41
60
 
42
- For development with file watching:
61
+ The published package name is `@tinyrack/devsync`, and the installed command is `devsync`.
62
+
63
+ ## 3. Quickstart
64
+
65
+ Initialize a local sync repository:
43
66
 
44
67
  ```bash
45
- npm run dev
68
+ devsync init
46
69
  ```
47
70
 
48
- The published package name is `@tinyrack/devsync`, but the installed command is still `devsync`.
71
+ Track a few configs:
72
+
73
+ ```bash
74
+ devsync add ~/.gitconfig
75
+ devsync add ~/.zshrc
76
+ devsync add ~/.config/mytool --secret
77
+ ```
49
78
 
50
- If you want the `devsync` command available from this checkout during development:
79
+ Review what would be captured:
51
80
 
52
81
  ```bash
53
- npm link
82
+ devsync status
83
+ devsync push --dry-run
54
84
  ```
55
85
 
56
- ## Release
86
+ Write your current local config into the sync repository:
87
+
88
+ ```bash
89
+ devsync push
90
+ ```
57
91
 
58
- - CI runs on every push and pull request with `npm run check` on Node.js 24.
59
- - npm publishing runs automatically when a Git tag matching `v*.*.*` is pushed.
60
- - The release workflow uses npm Trusted Publishing, so npm access is granted through GitHub Actions OIDC instead of an `NPM_TOKEN` secret.
61
- - The release workflow fails if the pushed tag does not match `package.json` `version`.
92
+ Open the sync repository and publish it with git:
62
93
 
63
- Typical release flow:
94
+ ```bash
95
+ devsync cd
96
+ git status
97
+ git add .
98
+ git commit -m "Update synced config"
99
+ git push
100
+ ```
101
+
102
+ On another machine, clone and restore from the same repo:
64
103
 
65
104
  ```bash
66
- npm version patch
67
- git push --follow-tags
105
+ devsync init https://example.com/my-sync-repo.git
106
+ devsync status
107
+ devsync pull --dry-run
108
+ devsync pull
68
109
  ```
69
110
 
70
- ## Storage Layout
111
+ Notes:
112
+
113
+ - `push` updates the sync repository contents only; it does not create git commits or push to a remote.
114
+ - `pull` updates local files only.
115
+ - Secret paths are stored encrypted in the repository and require the configured `age` identity to decrypt on restore.
116
+
117
+ ## 4. Detailed docs
118
+
119
+ ### How tracking works
120
+
121
+ - You add files or directories that live under your home directory.
122
+ - `devsync` mirrors them into `~/.config/devsync/sync/default/<repoPath>` for the default profile, or `~/.config/devsync/sync/<profile>/<repoPath>` for a named profile.
123
+ - Plain artifacts are stored as-is.
124
+ - Secret artifacts are stored with the `.devsync.secret` suffix.
125
+
126
+ Storage layout:
71
127
 
72
128
  - Sync repo: `~/.config/devsync/sync`
73
- - Default age identity file: `$XDG_CONFIG_HOME/devsync/age/keys.txt`
74
- - Tracked artifacts live under `~/.config/devsync/sync/files`
75
- - Secret file artifacts use the suffix `.devsync.secret`, for example `token.json.devsync.secret`
129
+ - Default profile artifacts: `~/.config/devsync/sync/default/<repoPath>`
130
+ - Named profile artifacts: `~/.config/devsync/sync/<profile>/<repoPath>`
131
+ - Default age identity: `$XDG_CONFIG_HOME/devsync/age/keys.txt`
132
+
133
+ ### Sync modes
76
134
 
77
- ## Usage
135
+ Each tracked path can use one of three modes:
136
+
137
+ - `normal`: store and restore plain content
138
+ - `secret`: encrypt before storing in the repo
139
+ - `ignore`: skip during push and pull
140
+
141
+ You can apply modes to tracked roots or nested paths inside tracked directories.
142
+
143
+ Profile-specific behavior inherits the tracked root mode and only changes nested paths inside tracked directories.
144
+
145
+ Examples:
78
146
 
79
147
  ```bash
80
- devsync <command>
148
+ devsync set secret ~/.config/mytool/token.json
149
+ devsync set ignore ~/.config/mytool/cache --recursive
150
+ devsync set normal ~/.config/mytool/public.json
151
+ devsync set secret ~/.config/mytool/token.json --profile work
81
152
  ```
82
153
 
83
- Or without linking:
154
+ ### Profile-specific overrides
155
+
156
+ - Track the root once without `--profile`.
157
+ - Use `devsync set --profile <name>` only for child paths inside tracked directories.
158
+ - Profile-specific rules inherit the parent root mode and only override nested paths.
159
+ - Named profile artifacts are stored under `<profile>/<repoPath>`.
160
+ - `default` is reserved for the base layout and cannot be used as a named profile.
161
+ - Standalone profiled roots and profiled file entries are not supported.
162
+
163
+ Example `config.json`:
164
+
165
+ ```json
166
+ {
167
+ "version": 1,
168
+ "age": {
169
+ "identityFile": "$XDG_CONFIG_HOME/devsync/age/keys.txt",
170
+ "recipients": ["age1example..."]
171
+ },
172
+ "entries": [
173
+ {
174
+ "kind": "directory",
175
+ "localPath": "~/.config/mytool",
176
+ "mode": "normal",
177
+ "overrides": {
178
+ "cache/": "ignore"
179
+ },
180
+ "profiles": {
181
+ "work": {
182
+ "overrides": {
183
+ "token.json": "secret"
184
+ }
185
+ }
186
+ },
187
+ "repoPath": ".config/mytool"
188
+ }
189
+ ]
190
+ }
191
+ ```
192
+
193
+ ### Common workflow
194
+
195
+ Check what changed:
196
+
197
+ ```bash
198
+ devsync status
199
+ ```
200
+
201
+ Capture local config into the repository:
202
+
203
+ ```bash
204
+ devsync push
205
+ ```
206
+
207
+ Restore repository state onto the machine:
208
+
209
+ ```bash
210
+ devsync pull
211
+ ```
212
+
213
+ Use dry runs when you want to review first:
84
214
 
85
215
  ```bash
86
- npm run start -- <command>
216
+ devsync push --dry-run
217
+ devsync pull --dry-run
87
218
  ```
88
219
 
89
- ## Commands
220
+ ### Command reference
90
221
 
91
- ### `init`
222
+ #### `init`
92
223
 
93
- Initialize the git-backed sync directory.
224
+ Create or connect the local sync repository.
94
225
 
95
226
  ```bash
96
227
  devsync init
@@ -98,91 +229,115 @@ devsync init https://example.com/my-sync-repo.git
98
229
  devsync init --identity "$XDG_CONFIG_HOME/devsync/age/keys.txt" --recipient age1...
99
230
  ```
100
231
 
101
- ### `add`
232
+ #### `add`
102
233
 
103
- Track a local file or directory under your home directory.
234
+ Track a file or directory under your home directory.
104
235
 
105
236
  ```bash
106
237
  devsync add ~/.gitconfig
107
- devsync add ./.zshrc
238
+ devsync add ~/.config/mytool
108
239
  devsync add ~/.config/mytool --secret
109
240
  ```
110
241
 
111
- ### `set`
242
+ `add --profile` is not supported for tracked roots. Track the root first, then use `set --profile` for child overrides.
243
+
244
+ #### `set`
112
245
 
113
- Set mode for a tracked directory root, child file, or child subtree.
246
+ Change the sync mode for a tracked root, child path, or subtree.
114
247
 
115
248
  ```bash
116
249
  devsync set secret ~/.config/mytool/token.json
117
250
  devsync set ignore ~/.config/mytool/cache --recursive
118
- cd ~/.ssh && devsync set ignore known_hosts
119
- devsync set normal .config/mytool/public.json
251
+ devsync set secret ~/.config/mytool/token.json --profile work
120
252
  ```
121
253
 
122
- ### `forget`
254
+ With `--profile`, the target must be a child path inside a tracked directory, and `default` cannot be used as the profile name. Profile-specific root mode changes are not supported.
123
255
 
124
- Remove a tracked local path or repository path from sync config.
256
+ #### `forget`
257
+
258
+ Remove a tracked path or nested override from config.
125
259
 
126
260
  ```bash
127
261
  devsync forget ~/.gitconfig
128
- cd ~/mytool && devsync forget ./settings.json
129
- devsync forget .config/mytool
262
+ devsync forget ~/.config/mytool
263
+ devsync forget .config/mytool/token.json --profile work
130
264
  ```
131
265
 
132
- ### `list`
266
+ #### `list`
133
267
 
134
- Show tracked entries, modes, and override rules.
268
+ Show tracked entries, default modes, root overrides, and profile-specific child overrides.
135
269
 
136
270
  ```bash
137
271
  devsync list
138
272
  ```
139
273
 
140
- ### `status`
274
+ #### `status`
141
275
 
142
- Show planned push and pull changes for the current sync config.
276
+ Preview planned push and pull changes.
143
277
 
144
278
  ```bash
145
279
  devsync status
146
280
  ```
147
281
 
148
- ### `doctor`
282
+ #### `doctor`
149
283
 
150
- Check the sync repository, config, age identity, and tracked local paths.
284
+ Validate repo state, config, tracked paths, and secret setup.
151
285
 
152
286
  ```bash
153
287
  devsync doctor
154
288
  ```
155
289
 
156
- ### `push`
290
+ #### `push`
157
291
 
158
- Mirror local config into the sync repository.
292
+ Write local state into the sync repository.
159
293
 
160
294
  ```bash
161
295
  devsync push
162
296
  devsync push --dry-run
163
297
  ```
164
298
 
165
- ### `pull`
299
+ #### `pull`
166
300
 
167
- Apply the sync repository to local config paths.
301
+ Apply repository state back onto the local machine.
168
302
 
169
303
  ```bash
170
304
  devsync pull
171
305
  devsync pull --dry-run
172
306
  ```
173
307
 
174
- ### `cd`
308
+ #### `cd`
175
309
 
176
- Print the sync directory in non-interactive mode, or open a shell there in interactive mode.
310
+ Open the sync repository in your shell, or print its path.
177
311
 
178
312
  ```bash
179
313
  devsync cd
180
314
  devsync cd --print
181
315
  ```
182
316
 
317
+ For flag-level details, use built-in help:
318
+
319
+ ```bash
320
+ devsync --help
321
+ devsync init --help
322
+ devsync add --help
323
+ devsync set --help
324
+ ```
325
+
183
326
  ## Development
184
327
 
185
- Validation commands:
328
+ Run the CLI locally:
329
+
330
+ ```bash
331
+ npm run start -- --help
332
+ ```
333
+
334
+ Watch mode:
335
+
336
+ ```bash
337
+ npm run dev
338
+ ```
339
+
340
+ Validation:
186
341
 
187
342
  ```bash
188
343
  npm run typecheck
@@ -190,8 +345,21 @@ biome check .
190
345
  npm run test
191
346
  ```
192
347
 
193
- Or run the full validation sequence:
348
+ Or run everything at once:
194
349
 
195
350
  ```bash
196
351
  npm run check
197
352
  ```
353
+
354
+ ## Release
355
+
356
+ - CI runs `npm run check` on every push and pull request.
357
+ - npm publishing runs automatically for Git tags matching `v*.*.*`.
358
+ - The release workflow expects the pushed tag to match `package.json` `version`.
359
+
360
+ Typical release flow:
361
+
362
+ ```bash
363
+ npm version patch
364
+ git push --follow-tags
365
+ ```
@@ -0,0 +1,14 @@
1
+ import { Command } from "@oclif/core";
2
+ type CommandError = Error & {
3
+ exitCode?: number;
4
+ oclif?: {
5
+ exit?: number;
6
+ };
7
+ };
8
+ export declare abstract class BaseCommand extends Command {
9
+ protected print(output: string): void;
10
+ protected printError(message: Error | string): void;
11
+ catch(error: CommandError): Promise<unknown>;
12
+ }
13
+ export {};
14
+ //# sourceMappingURL=base-command.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"base-command.d.ts","sourceRoot":"","sources":["../../src/cli/base-command.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,aAAa,CAAC;AAStC,KAAK,YAAY,GAAG,KAAK,GAAG;IAC1B,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,KAAK,CAAC,EAAE;QACN,IAAI,CAAC,EAAE,MAAM,CAAC;KACf,CAAC;CACH,CAAC;AAMF,8BAAsB,WAAY,SAAQ,OAAO;IAC/C,SAAS,CAAC,KAAK,CAAC,MAAM,EAAE,MAAM;IAI9B,SAAS,CAAC,UAAU,CAAC,OAAO,EAAE,KAAK,GAAG,MAAM;IAItB,KAAK,CAAC,KAAK,EAAE,YAAY,GAAG,OAAO,CAAC,OAAO,CAAC;CAQnE"}
@@ -0,0 +1,22 @@
1
+ import { Command } from "@oclif/core";
2
+ import { formatErrorMessage, writeStderr, writeStdout, } from "#app/lib/output.js";
3
+ import { formatDevsyncError } from "#app/services/error.js";
4
+ const resolveExitCode = (error) => {
5
+ return error.oclif?.exit ?? error.exitCode ?? 1;
6
+ };
7
+ export class BaseCommand extends Command {
8
+ print(output) {
9
+ writeStdout(output);
10
+ }
11
+ printError(message) {
12
+ writeStderr(formatErrorMessage(message));
13
+ }
14
+ async catch(error) {
15
+ if (error instanceof Error) {
16
+ this.printError(formatDevsyncError(error));
17
+ this.exit(resolveExitCode(error));
18
+ }
19
+ return super.catch(error);
20
+ }
21
+ }
22
+ //# sourceMappingURL=base-command.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"base-command.js","sourceRoot":"","sources":["../../src/cli/base-command.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,aAAa,CAAC;AAEtC,OAAO,EACL,kBAAkB,EAClB,WAAW,EACX,WAAW,GACZ,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EAAE,kBAAkB,EAAE,MAAM,wBAAwB,CAAC;AAS5D,MAAM,eAAe,GAAG,CAAC,KAAmB,EAAE,EAAE;IAC9C,OAAO,KAAK,CAAC,KAAK,EAAE,IAAI,IAAI,KAAK,CAAC,QAAQ,IAAI,CAAC,CAAC;AAClD,CAAC,CAAC;AAEF,MAAM,OAAgB,WAAY,SAAQ,OAAO;IACrC,KAAK,CAAC,MAAc;QAC5B,WAAW,CAAC,MAAM,CAAC,CAAC;IACtB,CAAC;IAES,UAAU,CAAC,OAAuB;QAC1C,WAAW,CAAC,kBAAkB,CAAC,OAAO,CAAC,CAAC,CAAC;IAC3C,CAAC;IAEe,KAAK,CAAC,KAAK,CAAC,KAAmB;QAC7C,IAAI,KAAK,YAAY,KAAK,EAAE,CAAC;YAC3B,IAAI,CAAC,UAAU,CAAC,kBAAkB,CAAC,KAAK,CAAC,CAAC,CAAC;YAC3C,IAAI,CAAC,IAAI,CAAC,eAAe,CAAC,KAAK,CAAC,CAAC,CAAC;QACpC,CAAC;QAED,OAAO,KAAK,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;IAC5B,CAAC;CACF"}
@@ -0,0 +1,8 @@
1
+ import { BaseCommand } from "#app/cli/base-command.js";
2
+ export default class SyncDir extends BaseCommand {
3
+ static summary: string;
4
+ static description: string;
5
+ static examples: string[];
6
+ run(): Promise<void>;
7
+ }
8
+ //# sourceMappingURL=dir.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dir.d.ts","sourceRoot":"","sources":["../../../src/cli/commands/dir.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,0BAA0B,CAAC;AAIvD,MAAM,CAAC,OAAO,OAAO,OAAQ,SAAQ,WAAW;IAC9C,OAAuB,OAAO,SAAmC;IAEjE,OAAuB,WAAW,SAC+G;IAEjJ,OAAuB,QAAQ,WAG7B;IAEoB,GAAG,IAAI,OAAO,CAAC,IAAI,CAAC;CAI3C"}
@@ -0,0 +1,16 @@
1
+ import { BaseCommand } from "#app/cli/base-command.js";
2
+ import { resolveDevsyncSyncDirectory } from "#app/config/xdg.js";
3
+ import { output } from "#app/lib/output.js";
4
+ export default class SyncDir extends BaseCommand {
5
+ static summary = "Print the sync directory path";
6
+ static description = 'Print the absolute path of the local sync repository directory. Compose with your shell to navigate there, for example: cd "$(devsync dir)".';
7
+ static examples = [
8
+ "<%= config.bin %> <%= command.id %>",
9
+ 'cd "$(<%= config.bin %> <%= command.id %>)"',
10
+ ];
11
+ async run() {
12
+ const syncDirectory = resolveDevsyncSyncDirectory();
13
+ this.print(output(syncDirectory));
14
+ }
15
+ }
16
+ //# sourceMappingURL=dir.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dir.js","sourceRoot":"","sources":["../../../src/cli/commands/dir.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,0BAA0B,CAAC;AACvD,OAAO,EAAE,2BAA2B,EAAE,MAAM,oBAAoB,CAAC;AACjE,OAAO,EAAE,MAAM,EAAE,MAAM,oBAAoB,CAAC;AAE5C,MAAM,CAAC,OAAO,OAAO,OAAQ,SAAQ,WAAW;IACvC,MAAM,CAAU,OAAO,GAAG,+BAA+B,CAAC;IAE1D,MAAM,CAAU,WAAW,GAChC,8IAA8I,CAAC;IAE1I,MAAM,CAAU,QAAQ,GAAG;QAChC,qCAAqC;QACrC,6CAA6C;KAC9C,CAAC;IAEc,KAAK,CAAC,GAAG;QACvB,MAAM,aAAa,GAAG,2BAA2B,EAAE,CAAC;QACpD,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,aAAa,CAAC,CAAC,CAAC;IACpC,CAAC"}
@@ -0,0 +1,8 @@
1
+ import { BaseCommand } from "#app/cli/base-command.js";
2
+ export default class SyncDoctor extends BaseCommand {
3
+ static summary: string;
4
+ static description: string;
5
+ static examples: string[];
6
+ run(): Promise<void>;
7
+ }
8
+ //# sourceMappingURL=doctor.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"doctor.d.ts","sourceRoot":"","sources":["../../../src/cli/commands/doctor.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,0BAA0B,CAAC;AAKvD,MAAM,CAAC,OAAO,OAAO,UAAW,SAAQ,WAAW;IACjD,OAAuB,OAAO,SAC2C;IAEzE,OAAuB,WAAW,SACsK;IAExM,OAAuB,QAAQ,WAA2C;IAEpD,GAAG,IAAI,OAAO,CAAC,IAAI,CAAC;CAU3C"}
@@ -0,0 +1,18 @@
1
+ import { BaseCommand } from "#app/cli/base-command.js";
2
+ import { formatSyncDoctorResult } from "#app/lib/output.js";
3
+ import { runSyncDoctor } from "#app/services/doctor.js";
4
+ import { createSyncContext } from "#app/services/runtime.js";
5
+ export default class SyncDoctor extends BaseCommand {
6
+ static summary = "Check sync repository, config, age identity, and tracked local paths";
7
+ static description = "Run health checks for the local sync setup, including repository availability, config validity, age identity configuration, and whether tracked local paths still exist where devsync expects them.";
8
+ static examples = ["<%= config.bin %> <%= command.id %>"];
9
+ async run() {
10
+ await this.parse(SyncDoctor);
11
+ const result = await runSyncDoctor(createSyncContext());
12
+ this.print(formatSyncDoctorResult(result));
13
+ if (result.hasFailures) {
14
+ this.exit(1);
15
+ }
16
+ }
17
+ }
18
+ //# sourceMappingURL=doctor.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"doctor.js","sourceRoot":"","sources":["../../../src/cli/commands/doctor.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,0BAA0B,CAAC;AACvD,OAAO,EAAE,sBAAsB,EAAE,MAAM,oBAAoB,CAAC;AAC5D,OAAO,EAAE,aAAa,EAAE,MAAM,yBAAyB,CAAC;AACxD,OAAO,EAAE,iBAAiB,EAAE,MAAM,0BAA0B,CAAC;AAE7D,MAAM,CAAC,OAAO,OAAO,UAAW,SAAQ,WAAW;IAC1C,MAAM,CAAU,OAAO,GAC5B,sEAAsE,CAAC;IAElE,MAAM,CAAU,WAAW,GAChC,qMAAqM,CAAC;IAEjM,MAAM,CAAU,QAAQ,GAAG,CAAC,qCAAqC,CAAC,CAAC;IAE1D,KAAK,CAAC,GAAG;QACvB,MAAM,IAAI,CAAC,KAAK,CAAC,UAAU,CAAC,CAAC;QAC7B,MAAM,MAAM,GAAG,MAAM,aAAa,CAAC,iBAAiB,EAAE,CAAC,CAAC;QAExD,IAAI,CAAC,KAAK,CAAC,sBAAsB,CAAC,MAAM,CAAC,CAAC,CAAC;QAE3C,IAAI,MAAM,CAAC,WAAW,EAAE,CAAC;YACvB,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;QACf,CAAC;IACH,CAAC"}
@@ -0,0 +1,23 @@
1
+ import SyncDir from "#app/cli/commands/dir.js";
2
+ import SyncDoctor from "#app/cli/commands/doctor.js";
3
+ import SyncInit from "#app/cli/commands/init.js";
4
+ import SyncMachineList from "#app/cli/commands/machine/list.js";
5
+ import SyncMachineUse from "#app/cli/commands/machine/use.js";
6
+ import SyncPull from "#app/cli/commands/pull.js";
7
+ import SyncPush from "#app/cli/commands/push.js";
8
+ import SyncStatus from "#app/cli/commands/status.js";
9
+ import SyncTrack from "#app/cli/commands/track.js";
10
+ import SyncUntrack from "#app/cli/commands/untrack.js";
11
+ export declare const COMMANDS: {
12
+ dir: typeof SyncDir;
13
+ doctor: typeof SyncDoctor;
14
+ init: typeof SyncInit;
15
+ "machine:list": typeof SyncMachineList;
16
+ "machine:use": typeof SyncMachineUse;
17
+ pull: typeof SyncPull;
18
+ push: typeof SyncPush;
19
+ status: typeof SyncStatus;
20
+ track: typeof SyncTrack;
21
+ untrack: typeof SyncUntrack;
22
+ };
23
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/cli/commands/index.ts"],"names":[],"mappings":"AAAA,OAAO,OAAO,MAAM,0BAA0B,CAAC;AAC/C,OAAO,UAAU,MAAM,6BAA6B,CAAC;AACrD,OAAO,QAAQ,MAAM,2BAA2B,CAAC;AACjD,OAAO,eAAe,MAAM,mCAAmC,CAAC;AAChE,OAAO,cAAc,MAAM,kCAAkC,CAAC;AAC9D,OAAO,QAAQ,MAAM,2BAA2B,CAAC;AACjD,OAAO,QAAQ,MAAM,2BAA2B,CAAC;AACjD,OAAO,UAAU,MAAM,6BAA6B,CAAC;AACrD,OAAO,SAAS,MAAM,4BAA4B,CAAC;AACnD,OAAO,WAAW,MAAM,8BAA8B,CAAC;AAEvD,eAAO,MAAM,QAAQ;;;;;;;;;;;CAWpB,CAAC"}
@@ -0,0 +1,23 @@
1
+ import SyncDir from "#app/cli/commands/dir.js";
2
+ import SyncDoctor from "#app/cli/commands/doctor.js";
3
+ import SyncInit from "#app/cli/commands/init.js";
4
+ import SyncMachineList from "#app/cli/commands/machine/list.js";
5
+ import SyncMachineUse from "#app/cli/commands/machine/use.js";
6
+ import SyncPull from "#app/cli/commands/pull.js";
7
+ import SyncPush from "#app/cli/commands/push.js";
8
+ import SyncStatus from "#app/cli/commands/status.js";
9
+ import SyncTrack from "#app/cli/commands/track.js";
10
+ import SyncUntrack from "#app/cli/commands/untrack.js";
11
+ export const COMMANDS = {
12
+ dir: SyncDir,
13
+ doctor: SyncDoctor,
14
+ init: SyncInit,
15
+ "machine:list": SyncMachineList,
16
+ "machine:use": SyncMachineUse,
17
+ pull: SyncPull,
18
+ push: SyncPush,
19
+ status: SyncStatus,
20
+ track: SyncTrack,
21
+ untrack: SyncUntrack,
22
+ };
23
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/cli/commands/index.ts"],"names":[],"mappings":"AAAA,OAAO,OAAO,MAAM,0BAA0B,CAAC;AAC/C,OAAO,UAAU,MAAM,6BAA6B,CAAC;AACrD,OAAO,QAAQ,MAAM,2BAA2B,CAAC;AACjD,OAAO,eAAe,MAAM,mCAAmC,CAAC;AAChE,OAAO,cAAc,MAAM,kCAAkC,CAAC;AAC9D,OAAO,QAAQ,MAAM,2BAA2B,CAAC;AACjD,OAAO,QAAQ,MAAM,2BAA2B,CAAC;AACjD,OAAO,UAAU,MAAM,6BAA6B,CAAC;AACrD,OAAO,SAAS,MAAM,4BAA4B,CAAC;AACnD,OAAO,WAAW,MAAM,8BAA8B,CAAC;AAEvD,MAAM,CAAC,MAAM,QAAQ,GAAG;IACtB,GAAG,EAAE,OAAO;IACZ,MAAM,EAAE,UAAU;IAClB,IAAI,EAAE,QAAQ;IACd,cAAc,EAAE,eAAe;IAC/B,aAAa,EAAE,cAAc;IAC7B,IAAI,EAAE,QAAQ;IACd,IAAI,EAAE,QAAQ;IACd,MAAM,EAAE,UAAU;IAClB,KAAK,EAAE,SAAS;IAChB,OAAO,EAAE,WAAW;CACrB,CAAC"}
@@ -0,0 +1,15 @@
1
+ import { BaseCommand } from "#app/cli/base-command.js";
2
+ export default class SyncInit extends BaseCommand {
3
+ static summary: string;
4
+ static description: string;
5
+ static examples: string[];
6
+ static args: {
7
+ repository: import("@oclif/core/interfaces").Arg<string | undefined, Record<string, unknown>>;
8
+ };
9
+ static flags: {
10
+ identity: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
11
+ recipient: import("@oclif/core/interfaces").OptionFlag<string[] | undefined, import("@oclif/core/interfaces").CustomOptions>;
12
+ };
13
+ run(): Promise<void>;
14
+ }
15
+ //# sourceMappingURL=init.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"init.d.ts","sourceRoot":"","sources":["../../../src/cli/commands/init.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,WAAW,EAAE,MAAM,0BAA0B,CAAC;AAKvD,MAAM,CAAC,OAAO,OAAO,QAAS,SAAQ,WAAW;IAC/C,OAAuB,OAAO,SAA8C;IAE5E,OAAuB,WAAW,SAC8N;IAEhQ,OAAuB,QAAQ,WAI7B;IAEF,OAAuB,IAAI;;MAKzB;IAEF,OAAuB,KAAK;;;MAc1B;IAEoB,GAAG,IAAI,OAAO,CAAC,IAAI,CAAC;CAe3C"}
@@ -0,0 +1,43 @@
1
+ import { Args, Flags } from "@oclif/core";
2
+ import { BaseCommand } from "#app/cli/base-command.js";
3
+ import { formatSyncInitResult } from "#app/lib/output.js";
4
+ import { initializeSync } from "#app/services/init.js";
5
+ import { createSyncContext } from "#app/services/runtime.js";
6
+ export default class SyncInit extends BaseCommand {
7
+ static summary = "Initialize the git-backed sync directory";
8
+ static description = "Create or connect the local devsync repository under your XDG config directory, then store the sync settings used by later pull and push operations. If you omit the repository argument, devsync initializes a local git repository in the sync directory.";
9
+ static examples = [
10
+ "<%= config.bin %> <%= command.id %>",
11
+ "<%= config.bin %> <%= command.id %> https://example.com/my-sync-repo.git",
12
+ '<%= config.bin %> <%= command.id %> --identity "$XDG_CONFIG_HOME/devsync/age/keys.txt" --recipient age1...',
13
+ ];
14
+ static args = {
15
+ repository: Args.string({
16
+ description: "Remote URL or local git repository path to clone",
17
+ required: false,
18
+ }),
19
+ };
20
+ static flags = {
21
+ identity: Flags.string({
22
+ helpValue: "path",
23
+ summary: "Persist an age identity file path",
24
+ description: "Store the age identity file path in manifest.json so later pull operations know which private key file to use for decrypting secret artifacts.",
25
+ }),
26
+ recipient: Flags.string({
27
+ helpValue: "recipient",
28
+ summary: "Persist an age recipient public key",
29
+ description: "Add an age recipient public key to manifest.json. Repeat this flag to encrypt secrets for multiple recipients during push operations.",
30
+ multiple: true,
31
+ }),
32
+ };
33
+ async run() {
34
+ const { args, flags } = await this.parse(SyncInit);
35
+ const output = formatSyncInitResult(await initializeSync({
36
+ identityFile: flags.identity,
37
+ recipients: flags.recipient ?? [],
38
+ repository: args.repository,
39
+ }, createSyncContext()));
40
+ this.print(output);
41
+ }
42
+ }
43
+ //# sourceMappingURL=init.js.map