dsh-graft-plugin 0.2.0 → 0.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.
package/src/auto-sync.js CHANGED
@@ -71,6 +71,10 @@ const DEFAULT_WRITE_TOOLS = ['write', 'edit', 'str_replace_editor', 'pwsh', 'bas
71
71
 
72
72
  /** Graft's own state helpers, loaded from wherever graft is installed. */
73
73
  async function loadGraftState(cliPath) {
74
+ // A cli we cannot locate (bare shim on PATH) must degrade to "unavailable",
75
+ // never throw: this runs inside fire-and-forget middleware and a rejection
76
+ // here is fatal to dsh.
77
+ if (typeof cliPath !== 'string' || cliPath === '') return undefined
74
78
  const claudeDir = join(dirname(cliPath), 'claude')
75
79
  const statePath = join(claudeDir, 'state.js')
76
80
  const syncRun = join(claudeDir, 'sync-run.js')
@@ -132,7 +136,11 @@ export function createAutoSync(deps = {}) {
132
136
  let loaded
133
137
  const graft = async () => {
134
138
  if (loaded === undefined) {
135
- loaded = deps.graft !== undefined ? deps.graft : await loadGraftState(deps.cliPath)
139
+ try {
140
+ loaded = deps.graft !== undefined ? await deps.graft : await loadGraftState(deps.cliPath)
141
+ } catch {
142
+ loaded = null // any loader failure counts as "graft unavailable"
143
+ }
136
144
  if (loaded === undefined) loaded = null
137
145
  }
138
146
  return loaded
@@ -161,7 +169,7 @@ export function createAutoSync(deps = {}) {
161
169
  root,
162
170
  setTimer(() => {
163
171
  timers.delete(root)
164
- void api.syncIfDirty(root)
172
+ void api.syncIfDirty(root).catch(() => {})
165
173
  }, debounceMs),
166
174
  )
167
175
  }
@@ -201,6 +209,30 @@ export function createAutoSync(deps = {}) {
201
209
  }
202
210
  if (stats?.dirty !== true && !pending.has(root)) return 'clean'
203
211
 
212
+ return api.rebuild(root)
213
+ },
214
+
215
+ /**
216
+ * Rebuild one repo NOW, whether or not it looks dirty.
217
+ *
218
+ * The popup's Build button lands here, not in syncIfDirty: its gate reads
219
+ * graft's `dirty` flag, and a repo rebuilt by a plain `graft build` — which
220
+ * never patches stats.json — can read clean while being stale, answering
221
+ * 'clean' and spawning nothing. A pressed button that changes nothing is
222
+ * the failure this exists to prevent. The lock and the interval floor
223
+ * still apply: forced means "skip the dirt check", not "race a running
224
+ * build".
225
+ *
226
+ * @returns why it did or did not run, for the harness and the log.
227
+ */
228
+ rebuild: async (root) => {
229
+ if (typeof root !== 'string' || root === '') return 'no-root'
230
+ const g = await graft()
231
+ if (g === null) return 'graft-unavailable'
232
+
233
+ const since = now() - (lastRun.get(root) ?? -Infinity)
234
+ if (since < minIntervalMs) return 'too-soon'
235
+
204
236
  // The lock is graft's, so a rebuild already running — started here, or by
205
237
  // Claude Code in the same repo — is left alone rather than raced.
206
238
  try {