dsh-webdav-sync 1.0.3 → 1.0.4
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 +38 -19
- package/lib/client.js +109 -31
- package/lib/dirs.d.ts +1 -1
- package/lib/dirs.js +1 -1
- package/lib/engine.d.ts +22 -5
- package/lib/engine.js +88 -24
- package/lib/index.js +22 -3
- package/lib/keys.d.ts +125 -0
- package/lib/keys.js +168 -0
- package/lib/state.js +19 -6
- package/lib/store.d.ts +18 -8
- package/lib/store.js +23 -33
- package/lib/wire.d.ts +88 -16
- package/lib/wire.js +12 -1
- package/package.json +2 -2
package/lib/state.js
CHANGED
|
@@ -38,11 +38,19 @@ export function defaultConfig() {
|
|
|
38
38
|
passphrase: '',
|
|
39
39
|
autoEvery: AUTO_MANUAL,
|
|
40
40
|
models: true,
|
|
41
|
-
defaults: true,
|
|
42
41
|
plugins: true,
|
|
43
42
|
patch: true,
|
|
43
|
+
// On by default: the profile names where its API keys come from, so a setup
|
|
44
|
+
// that travels without them arrives with every provider unauthenticated. The
|
|
45
|
+
// switch exists so a person can keep secrets off the server; see
|
|
46
|
+
// `docs/decisions/0054-sync-api-keys.md`.
|
|
47
|
+
keys: true,
|
|
44
48
|
dirs: [],
|
|
45
|
-
|
|
49
|
+
// Off by default: a directory travels whole, and a fresh setup that has not
|
|
50
|
+
// named one yet has nothing to carry. The list is kept independently, so
|
|
51
|
+
// switching this on or off never forgets a path — see
|
|
52
|
+
// `docs/decisions/0056-drop-default-model-part.md`.
|
|
53
|
+
dirsEnabled: false,
|
|
46
54
|
rememberPassword: true,
|
|
47
55
|
headers: '',
|
|
48
56
|
relayEnabled: false,
|
|
@@ -211,13 +219,11 @@ export function syncsPart(config, part) {
|
|
|
211
219
|
switch (part) {
|
|
212
220
|
case 'models':
|
|
213
221
|
return config.models;
|
|
214
|
-
case 'defaults':
|
|
215
|
-
return config.defaults;
|
|
216
222
|
case 'plugins':
|
|
217
223
|
return config.plugins;
|
|
218
224
|
case 'patch':
|
|
219
225
|
return config.patch;
|
|
220
|
-
// The extra directories carry a switch of their own, like the
|
|
226
|
+
// The extra directories carry a switch of their own, like the three parts above:
|
|
221
227
|
// the list of paths says *which* directories, the switch says whether this
|
|
222
228
|
// computer carries them at all. Off keeps the list — the paths are not
|
|
223
229
|
// forgotten — and changes nothing anywhere else: the part is carried through
|
|
@@ -225,6 +231,13 @@ export function syncsPart(config, part) {
|
|
|
225
231
|
// others.
|
|
226
232
|
case 'dirs':
|
|
227
233
|
return config.dirsEnabled;
|
|
234
|
+
// The API keys are the one part that is a secret, so they are the one part
|
|
235
|
+
// whose switch a person may genuinely want off: the server holds the sealed
|
|
236
|
+
// file, and a sealed file still sits on someone else's disk. Off leaves every
|
|
237
|
+
// key here and carries the server's copy of the part through untouched, so
|
|
238
|
+
// nothing is deleted anywhere either.
|
|
239
|
+
case 'keys':
|
|
240
|
+
return config.keys;
|
|
228
241
|
}
|
|
229
242
|
}
|
|
230
243
|
/** Fill in defaults and coerce the several fields that come from a browser. */
|
|
@@ -249,9 +262,9 @@ export function normaliseConfig(raw) {
|
|
|
249
262
|
passphrase: text('passphrase'),
|
|
250
263
|
autoEvery,
|
|
251
264
|
models: flag('models'),
|
|
252
|
-
defaults: flag('defaults'),
|
|
253
265
|
plugins: flag('plugins'),
|
|
254
266
|
patch: flag('patch'),
|
|
267
|
+
keys: flag('keys'),
|
|
255
268
|
dirs: Array.isArray(raw.dirs) ? normaliseDirs(raw.dirs) : base.dirs,
|
|
256
269
|
dirsEnabled: flag('dirsEnabled'),
|
|
257
270
|
rememberPassword: flag('rememberPassword'),
|
package/lib/store.d.ts
CHANGED
|
@@ -8,16 +8,20 @@
|
|
|
8
8
|
* everything else — MCP servers, permission presets, UI settings, inserts — is
|
|
9
9
|
* the rest of them. `package.json` names the installed plugins.
|
|
10
10
|
*
|
|
11
|
+
* The default-model row is read here only to be kept out of the sync: it is one of
|
|
12
|
+
* the rows a sync never carries and never writes, like this plugin's own row. See
|
|
13
|
+
* `docs/decisions/0056-drop-default-model-part.md`.
|
|
14
|
+
*
|
|
11
15
|
* Writes go through the `yaml` Document API rather than a stringify of a parsed
|
|
12
16
|
* object, so the file's own comments and layout survive a sync that only meant to
|
|
13
17
|
* change one provider.
|
|
14
18
|
*/
|
|
15
19
|
import type { Document } from 'yaml';
|
|
16
|
-
import { type
|
|
20
|
+
import { type KeysPart, type ModelsPart, type PatchEntry, type PatchPart, type PluginsPart } from './wire.js';
|
|
17
21
|
import type { ResolvedPaths } from './paths.js';
|
|
18
22
|
/** The profile patch row holding the model catalog. */
|
|
19
23
|
export declare const MODELS_ID = "llm-pi-ai";
|
|
20
|
-
/** The profile patch row holding the default model. */
|
|
24
|
+
/** The profile patch row holding the default model; kept out of every part. */
|
|
21
25
|
export declare const DEFAULTS_ID = "agent-default-model";
|
|
22
26
|
/** Package whose patch row the plugin adds for itself; never carried as a part. */
|
|
23
27
|
export declare const SELF_ID = "dsh-webdav-sync";
|
|
@@ -50,9 +54,15 @@ export declare function readManifest(paths: ResolvedPaths): Promise<ManifestFile
|
|
|
50
54
|
/** Every part of a profile this plugin can carry, as collected from disk. */
|
|
51
55
|
export interface Collected {
|
|
52
56
|
models?: ModelsPart;
|
|
53
|
-
defaults?: DefaultsPart;
|
|
54
57
|
plugins?: PluginsPart;
|
|
55
58
|
patch?: PatchPart;
|
|
59
|
+
/**
|
|
60
|
+
* The keys, added by `take` rather than here: they are the one part that does
|
|
61
|
+
* not come from a profile file. Left out entirely when this computer holds
|
|
62
|
+
* nothing for any name the profile uses — the same "no opinion" an absent
|
|
63
|
+
* `models` carries, so the server's copy is carried through untouched.
|
|
64
|
+
*/
|
|
65
|
+
keys?: KeysPart;
|
|
56
66
|
}
|
|
57
67
|
/**
|
|
58
68
|
* Gather the parts from one profile.
|
|
@@ -73,14 +83,14 @@ export declare function missingPlugins(plugins: PluginsPart | undefined, install
|
|
|
73
83
|
/**
|
|
74
84
|
* Rewrite the profile patch so it holds the merged parts.
|
|
75
85
|
*
|
|
76
|
-
*
|
|
77
|
-
*
|
|
78
|
-
*
|
|
79
|
-
* so a sync that changed nothing does not rewrite the
|
|
86
|
+
* The model row is replaced in place when it exists and appended when it does not;
|
|
87
|
+
* the remaining rows are replaced wholesale by the merged list, with the model, the
|
|
88
|
+
* default-model and this plugin's own rows kept where they were. A no-op merge
|
|
89
|
+
* leaves the bytes untouched, so a sync that changed nothing does not rewrite the
|
|
90
|
+
* file.
|
|
80
91
|
*/
|
|
81
92
|
export declare function applyPatch(patch: PatchFile, parts: {
|
|
82
93
|
models?: ModelsPart | null;
|
|
83
|
-
defaults?: DefaultsPart | null;
|
|
84
94
|
patch?: PatchPart;
|
|
85
95
|
}): string;
|
|
86
96
|
/**
|
package/lib/store.js
CHANGED
|
@@ -8,6 +8,10 @@
|
|
|
8
8
|
* everything else — MCP servers, permission presets, UI settings, inserts — is
|
|
9
9
|
* the rest of them. `package.json` names the installed plugins.
|
|
10
10
|
*
|
|
11
|
+
* The default-model row is read here only to be kept out of the sync: it is one of
|
|
12
|
+
* the rows a sync never carries and never writes, like this plugin's own row. See
|
|
13
|
+
* `docs/decisions/0056-drop-default-model-part.md`.
|
|
14
|
+
*
|
|
11
15
|
* Writes go through the `yaml` Document API rather than a stringify of a parsed
|
|
12
16
|
* object, so the file's own comments and layout survive a sync that only meant to
|
|
13
17
|
* change one provider.
|
|
@@ -20,7 +24,7 @@ import { hashJson } from './crypto.js';
|
|
|
20
24
|
import { BACKUP_EXT, } from './wire.js';
|
|
21
25
|
/** The profile patch row holding the model catalog. */
|
|
22
26
|
export const MODELS_ID = 'llm-pi-ai';
|
|
23
|
-
/** The profile patch row holding the default model. */
|
|
27
|
+
/** The profile patch row holding the default model; kept out of every part. */
|
|
24
28
|
export const DEFAULTS_ID = 'agent-default-model';
|
|
25
29
|
/** Package whose patch row the plugin adds for itself; never carried as a part. */
|
|
26
30
|
export const SELF_ID = 'dsh-webdav-sync';
|
|
@@ -97,11 +101,6 @@ export function collect(patch, manifest) {
|
|
|
97
101
|
const config = modelsRow.value.config;
|
|
98
102
|
out.models = { config: isPlainObject(config) ? config : {} };
|
|
99
103
|
}
|
|
100
|
-
const defaultsRow = patch.rows.find((row) => row.key === `id:${DEFAULTS_ID}`);
|
|
101
|
-
if (defaultsRow !== undefined) {
|
|
102
|
-
const config = defaultsRow.value.config;
|
|
103
|
-
out.defaults = { config: isPlainObject(config) ? config : {} };
|
|
104
|
-
}
|
|
105
104
|
const dependencies = manifest.value.dependencies;
|
|
106
105
|
const bundles = readBundles(manifest.value);
|
|
107
106
|
if (isPlainObject(dependencies) || bundles.length > 0) {
|
|
@@ -152,10 +151,11 @@ export function missingPlugins(plugins, installed) {
|
|
|
152
151
|
/**
|
|
153
152
|
* Rewrite the profile patch so it holds the merged parts.
|
|
154
153
|
*
|
|
155
|
-
*
|
|
156
|
-
*
|
|
157
|
-
*
|
|
158
|
-
* so a sync that changed nothing does not rewrite the
|
|
154
|
+
* The model row is replaced in place when it exists and appended when it does not;
|
|
155
|
+
* the remaining rows are replaced wholesale by the merged list, with the model, the
|
|
156
|
+
* default-model and this plugin's own rows kept where they were. A no-op merge
|
|
157
|
+
* leaves the bytes untouched, so a sync that changed nothing does not rewrite the
|
|
158
|
+
* file.
|
|
159
159
|
*/
|
|
160
160
|
export function applyPatch(patch, parts) {
|
|
161
161
|
const document = patch.document;
|
|
@@ -165,9 +165,6 @@ export function applyPatch(patch, parts) {
|
|
|
165
165
|
if (parts.models !== undefined && parts.models !== null) {
|
|
166
166
|
rows.push({ id: MODELS_ID, name: '@deepseek-ai/dsh-llm-pi-ai', config: parts.models.config });
|
|
167
167
|
}
|
|
168
|
-
if (parts.defaults !== undefined && parts.defaults !== null) {
|
|
169
|
-
rows.push({ id: DEFAULTS_ID, name: '@deepseek-ai/dsh-agent-default-model', config: parts.defaults.config });
|
|
170
|
-
}
|
|
171
168
|
if (parts.patch !== undefined)
|
|
172
169
|
rows.push(...parts.patch.entries);
|
|
173
170
|
const replacement = document.createNode(rows);
|
|
@@ -187,15 +184,13 @@ export function applyPatch(patch, parts) {
|
|
|
187
184
|
const value = item.toJSON();
|
|
188
185
|
byKey.set(keyOf(value), { node: item, index });
|
|
189
186
|
});
|
|
190
|
-
// 1. Replace or append the
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
const existing = byKey.get(`id:${id}`);
|
|
198
|
-
if (part === null) {
|
|
187
|
+
// 1. Replace or append the model row. It is the one row with a part of its own;
|
|
188
|
+
// the default-model row that sits beside it is left exactly as it is, since
|
|
189
|
+
// it is not carried at all.
|
|
190
|
+
const modelPart = parts.models;
|
|
191
|
+
if (modelPart !== undefined) {
|
|
192
|
+
const existing = byKey.get(`id:${MODELS_ID}`);
|
|
193
|
+
if (modelPart === null) {
|
|
199
194
|
// The other computer took the row away, and that is the merge: a part that
|
|
200
195
|
// was removed there has to leave here, or the two never converge.
|
|
201
196
|
if (existing !== undefined) {
|
|
@@ -203,9 +198,8 @@ export function applyPatch(patch, parts) {
|
|
|
203
198
|
if (at !== -1)
|
|
204
199
|
contents.items.splice(at, 1);
|
|
205
200
|
}
|
|
206
|
-
continue;
|
|
207
201
|
}
|
|
208
|
-
if (existing !== undefined) {
|
|
202
|
+
else if (existing !== undefined) {
|
|
209
203
|
// Replace the config node only when it actually differs, and then merge
|
|
210
204
|
// into it rather than rebuilding: the catalog holds the user's own
|
|
211
205
|
// comments about their providers. `keepScalar` keeps the node itself.
|
|
@@ -217,16 +211,16 @@ export function applyPatch(patch, parts) {
|
|
|
217
211
|
// lacked the key does not gain an empty one.
|
|
218
212
|
const config = existing.node.has('config') ? existing.node.get('config', true) : undefined;
|
|
219
213
|
if (config === undefined) {
|
|
220
|
-
if (Object.keys(
|
|
221
|
-
existing.node.set('config', document.createNode(
|
|
214
|
+
if (Object.keys(modelPart.config).length > 0) {
|
|
215
|
+
existing.node.set('config', document.createNode(modelPart.config));
|
|
222
216
|
}
|
|
223
217
|
}
|
|
224
|
-
else if (!sameJson(config,
|
|
225
|
-
existing.node.set('config', mergeInto(document, config,
|
|
218
|
+
else if (!sameJson(config, modelPart.config)) {
|
|
219
|
+
existing.node.set('config', mergeInto(document, config, modelPart.config));
|
|
226
220
|
}
|
|
227
221
|
}
|
|
228
222
|
else {
|
|
229
|
-
const row = document.createNode({ id, name:
|
|
223
|
+
const row = document.createNode({ id: MODELS_ID, name: '@deepseek-ai/dsh-llm-pi-ai', config: modelPart.config });
|
|
230
224
|
contents.items.push(row);
|
|
231
225
|
}
|
|
232
226
|
}
|
|
@@ -275,10 +269,6 @@ export function applyPatch(patch, parts) {
|
|
|
275
269
|
return patch.text;
|
|
276
270
|
return document.toString();
|
|
277
271
|
}
|
|
278
|
-
/** The loader `name` a row carries when this plugin has to create it. */
|
|
279
|
-
function defaultNameFor(id) {
|
|
280
|
-
return id === MODELS_ID ? '@deepseek-ai/dsh-llm-pi-ai' : '@deepseek-ai/dsh-agent-default-model';
|
|
281
|
-
}
|
|
282
272
|
/**
|
|
283
273
|
* Rewrite the profile manifest with merged plugin declarations, two-space
|
|
284
274
|
* indented.
|
package/lib/wire.d.ts
CHANGED
|
@@ -62,8 +62,19 @@ export declare const LEGACY_REMOTE_FILE = "dsh-profile.dshsync";
|
|
|
62
62
|
* second computer gets both the path and what is under it. Its empty value is a
|
|
63
63
|
* real value (`{ dirs: [] }`), not an absent part, because "the list is now
|
|
64
64
|
* empty" is a change that has to travel like any other.
|
|
65
|
+
*
|
|
66
|
+
* `keys` is the one part whose content is a secret, and the one part that lives
|
|
67
|
+
* outside the profile: the API keys this computer's credential store holds for its
|
|
68
|
+
* providers. It is on by default, because a setup that travels without its keys
|
|
69
|
+
* arrives with every provider unauthenticated, and it can be switched off on its
|
|
70
|
+
* own; see {@link KeysPart}.
|
|
71
|
+
*
|
|
72
|
+
* The default model is deliberately *not* a part: which model a new session starts
|
|
73
|
+
* on is changed several times a day, and every change would push a file for
|
|
74
|
+
* something a person sets on each computer anyway. Its row stays out of `patch`
|
|
75
|
+
* too, like this plugin's own row. See `docs/decisions/0056-drop-default-model-part.md`.
|
|
65
76
|
*/
|
|
66
|
-
export declare const PARTS: readonly ["models", "
|
|
77
|
+
export declare const PARTS: readonly ["models", "plugins", "patch", "dirs", "keys"];
|
|
67
78
|
export type Part = (typeof PARTS)[number];
|
|
68
79
|
/**
|
|
69
80
|
* Most extra directories one configuration may name.
|
|
@@ -91,11 +102,6 @@ export interface ModelsPart {
|
|
|
91
102
|
/** The whole `llm-pi-ai` entry config, so fields beyond `providers` travel too. */
|
|
92
103
|
config: Record<string, unknown>;
|
|
93
104
|
}
|
|
94
|
-
/** The `defaults` part: which provider/model new sessions start on. */
|
|
95
|
-
export interface DefaultsPart {
|
|
96
|
-
/** The whole `agent-default-model` entry config. */
|
|
97
|
-
config: Record<string, unknown>;
|
|
98
|
-
}
|
|
99
105
|
/** One other top-level row of the profile patch, carried by the `patch` part. */
|
|
100
106
|
export interface PatchEntry {
|
|
101
107
|
/** `id` of an id-targeted row, or `insert` for an insert block. */
|
|
@@ -150,6 +156,20 @@ export interface DirSpec {
|
|
|
150
156
|
export interface DirsPart {
|
|
151
157
|
dirs: DirSpec[];
|
|
152
158
|
}
|
|
159
|
+
/**
|
|
160
|
+
* A credential this computer could not read or write, and why not.
|
|
161
|
+
*
|
|
162
|
+
* A store that cannot be read must not look like a store with nothing in it: an
|
|
163
|
+
* empty read would say "this computer has no keys", which is a real answer a sync
|
|
164
|
+
* acts on. This is how the two are told apart, the same way {@link DirProblem}
|
|
165
|
+
* keeps an unreadable directory from looking like an empty one.
|
|
166
|
+
*/
|
|
167
|
+
export interface KeyProblem {
|
|
168
|
+
/** The reference name, as the profile asks for it; '' when the store itself failed. */
|
|
169
|
+
ref: string;
|
|
170
|
+
/** What went wrong, in a sentence the card can show. */
|
|
171
|
+
message: string;
|
|
172
|
+
}
|
|
153
173
|
/** A configured directory this computer could not read, and why not. */
|
|
154
174
|
export interface DirProblem {
|
|
155
175
|
/** The configured path, as it is stored. */
|
|
@@ -167,6 +187,33 @@ export interface DirProblem {
|
|
|
167
187
|
*/
|
|
168
188
|
missing?: boolean;
|
|
169
189
|
}
|
|
190
|
+
/**
|
|
191
|
+
* The `keys` part: the API keys this computer holds for its providers.
|
|
192
|
+
*
|
|
193
|
+
* The profile names a credential instead of holding it — `apiKeyEnv: OFOX_API_KEY`
|
|
194
|
+
* on a provider route, `secretEnv` on a GitHub webhook — and the value behind that
|
|
195
|
+
* name lives in this computer's credential store (`$DSH_HOME/.credentials.yaml`).
|
|
196
|
+
* That is why a setup synced without this part arrives complete and still cannot
|
|
197
|
+
* talk to a single provider: it carries the names, and the names resolve to
|
|
198
|
+
* nothing here.
|
|
199
|
+
*
|
|
200
|
+
* A name is the whole address of a secret, so this part is a map and not a list:
|
|
201
|
+
* applying it puts each value back under the name the profile already asks for.
|
|
202
|
+
* Nothing here is ever deleted — a key removed on one computer stays on the others,
|
|
203
|
+
* because a credential a person stored by hand is not this plugin's to throw away.
|
|
204
|
+
* See `docs/decisions/0054-sync-api-keys.md`.
|
|
205
|
+
*/
|
|
206
|
+
export interface KeysPart {
|
|
207
|
+
/**
|
|
208
|
+
* Secret values by the reference name that resolves them, exactly as the
|
|
209
|
+
* credential store holds them (`apiKeyEnv`/`secretEnv` values, verbatim).
|
|
210
|
+
*
|
|
211
|
+
* Only names this computer actually has a stored value for appear; a name that
|
|
212
|
+
* falls through to the environment is left out, since the environment is the
|
|
213
|
+
* other computer's own business.
|
|
214
|
+
*/
|
|
215
|
+
refs: Record<string, string>;
|
|
216
|
+
}
|
|
170
217
|
/** Everything one sealed backup carries. Field names are the wire format. */
|
|
171
218
|
export interface Bundle {
|
|
172
219
|
format: 'dsh-webdav-sync-backup';
|
|
@@ -176,10 +223,10 @@ export interface Bundle {
|
|
|
176
223
|
/** Which app sealed it, for a file several tools might share a folder with. */
|
|
177
224
|
app: string;
|
|
178
225
|
models?: ModelsPart;
|
|
179
|
-
defaults?: DefaultsPart;
|
|
180
226
|
plugins?: PluginsPart;
|
|
181
227
|
patch?: PatchPart;
|
|
182
228
|
dirs?: DirsPart;
|
|
229
|
+
keys?: KeysPart;
|
|
183
230
|
}
|
|
184
231
|
/** The format word in the envelope; anything else on the server is not ours. */
|
|
185
232
|
export declare const BUNDLE_FORMAT = "dsh-webdav-sync-backup";
|
|
@@ -209,10 +256,10 @@ export interface Notice {
|
|
|
209
256
|
/** One side of the last completed sync, per part. */
|
|
210
257
|
export interface PartHashes {
|
|
211
258
|
models?: string;
|
|
212
|
-
defaults?: string;
|
|
213
259
|
plugins?: string;
|
|
214
260
|
patch?: string;
|
|
215
261
|
dirs?: string;
|
|
262
|
+
keys?: string;
|
|
216
263
|
}
|
|
217
264
|
/**
|
|
218
265
|
* The variable half of the state: everything a completed sync records.
|
|
@@ -276,12 +323,20 @@ export interface SyncConfig {
|
|
|
276
323
|
autoEvery: number;
|
|
277
324
|
/** Whether the model catalog travels. */
|
|
278
325
|
models: boolean;
|
|
279
|
-
/** Whether the default-model row travels. */
|
|
280
|
-
defaults: boolean;
|
|
281
326
|
/** Whether the installed-plugin list travels. */
|
|
282
327
|
plugins: boolean;
|
|
283
328
|
/** Whether the remaining profile-patch rows travel. */
|
|
284
329
|
patch: boolean;
|
|
330
|
+
/**
|
|
331
|
+
* Whether this computer's stored API keys travel.
|
|
332
|
+
*
|
|
333
|
+
* On by default: a provider route names its credential, so a setup synced
|
|
334
|
+
* without the keys arrives with every provider unauthenticated. Turning it off
|
|
335
|
+
* keeps this computer's keys off the server entirely — nothing is deleted
|
|
336
|
+
* anywhere, and the server's copy of the part is carried through untouched,
|
|
337
|
+
* exactly as for the three parts above.
|
|
338
|
+
*/
|
|
339
|
+
keys: boolean;
|
|
285
340
|
/**
|
|
286
341
|
* Absolute paths of the extra directories that travel with the setup, in the
|
|
287
342
|
* order the user added them. The list itself is the `dirs` part, so what is
|
|
@@ -292,10 +347,11 @@ export interface SyncConfig {
|
|
|
292
347
|
/**
|
|
293
348
|
* Whether the extra directories travel with this setup.
|
|
294
349
|
*
|
|
295
|
-
*
|
|
296
|
-
*
|
|
297
|
-
*
|
|
298
|
-
*
|
|
350
|
+
* Off is the resting state: a configuration that has not said otherwise carries
|
|
351
|
+
* no directory. The list above is kept either way: switching this off says "this
|
|
352
|
+
* computer does not carry them", not "forget them". Nothing is deleted anywhere
|
|
353
|
+
* by turning it off — the server's copy of the part is carried through
|
|
354
|
+
* untouched, exactly as for the other parts.
|
|
299
355
|
*/
|
|
300
356
|
dirsEnabled: boolean;
|
|
301
357
|
/** Whether the plugin's own address, user and password are kept for next time. */
|
|
@@ -339,9 +395,10 @@ export interface ConfigView {
|
|
|
339
395
|
passphraseSet: boolean;
|
|
340
396
|
autoEvery: number;
|
|
341
397
|
models: boolean;
|
|
342
|
-
defaults: boolean;
|
|
343
398
|
plugins: boolean;
|
|
344
399
|
patch: boolean;
|
|
400
|
+
/** Whether this computer's stored API keys travel. */
|
|
401
|
+
keys: boolean;
|
|
345
402
|
/** The extra directories that travel, as stored. Not a secret. */
|
|
346
403
|
dirs: string[];
|
|
347
404
|
/** Whether the extra directories travel with the current configuration. */
|
|
@@ -412,6 +469,13 @@ export interface StatusResult {
|
|
|
412
469
|
* this is how the card is able to say so.
|
|
413
470
|
*/
|
|
414
471
|
dirProblems?: DirProblem[];
|
|
472
|
+
/**
|
|
473
|
+
* Credentials the configuration names that could not be read here, with the
|
|
474
|
+
* reason. Present exactly when there is at least one, for the same reason as
|
|
475
|
+
* {@link StatusResult.dirProblems}: a store that cannot be read is left as the
|
|
476
|
+
* server has it for this sync rather than read as empty.
|
|
477
|
+
*/
|
|
478
|
+
keysProblems?: KeyProblem[];
|
|
415
479
|
/** The profile declares plugins the installed tree does not have yet. */
|
|
416
480
|
pendingInstall: boolean;
|
|
417
481
|
/** Which plugins those are, present exactly when {@link pendingInstall} is true. */
|
|
@@ -427,9 +491,10 @@ export interface ConfigureBody {
|
|
|
427
491
|
passphrase?: string;
|
|
428
492
|
autoEvery?: number;
|
|
429
493
|
models?: boolean;
|
|
430
|
-
defaults?: boolean;
|
|
431
494
|
plugins?: boolean;
|
|
432
495
|
patch?: boolean;
|
|
496
|
+
/** Whether this computer's stored API keys travel. */
|
|
497
|
+
keys?: boolean;
|
|
433
498
|
/** The whole list of extra directories, replacing the stored one. */
|
|
434
499
|
dirs?: string[];
|
|
435
500
|
/** Whether the extra directories travel with this setup. */
|
|
@@ -471,6 +536,13 @@ export interface SyncResult {
|
|
|
471
536
|
pendingNames?: string[];
|
|
472
537
|
/** The profile has to be reloaded for the applied configuration to take effect. */
|
|
473
538
|
reloadNeeded: boolean;
|
|
539
|
+
/**
|
|
540
|
+
* Keys the merge brought here that this computer's credential store would not
|
|
541
|
+
* take, with the reason. Present exactly when there is at least one: a key that
|
|
542
|
+
* could not be stored leaves that provider unauthenticated, and a sync that
|
|
543
|
+
* reported success would hide it.
|
|
544
|
+
*/
|
|
545
|
+
keysProblems?: KeyProblem[];
|
|
474
546
|
}
|
|
475
547
|
/** POST {@link ROUTE_CHECK} result. */
|
|
476
548
|
export interface CheckResult {
|
package/lib/wire.js
CHANGED
|
@@ -64,8 +64,19 @@ export const LEGACY_REMOTE_FILE = 'dsh-profile.dshsync';
|
|
|
64
64
|
* second computer gets both the path and what is under it. Its empty value is a
|
|
65
65
|
* real value (`{ dirs: [] }`), not an absent part, because "the list is now
|
|
66
66
|
* empty" is a change that has to travel like any other.
|
|
67
|
+
*
|
|
68
|
+
* `keys` is the one part whose content is a secret, and the one part that lives
|
|
69
|
+
* outside the profile: the API keys this computer's credential store holds for its
|
|
70
|
+
* providers. It is on by default, because a setup that travels without its keys
|
|
71
|
+
* arrives with every provider unauthenticated, and it can be switched off on its
|
|
72
|
+
* own; see {@link KeysPart}.
|
|
73
|
+
*
|
|
74
|
+
* The default model is deliberately *not* a part: which model a new session starts
|
|
75
|
+
* on is changed several times a day, and every change would push a file for
|
|
76
|
+
* something a person sets on each computer anyway. Its row stays out of `patch`
|
|
77
|
+
* too, like this plugin's own row. See `docs/decisions/0056-drop-default-model-part.md`.
|
|
67
78
|
*/
|
|
68
|
-
export const PARTS = ['models', '
|
|
79
|
+
export const PARTS = ['models', 'plugins', 'patch', 'dirs', 'keys'];
|
|
69
80
|
/**
|
|
70
81
|
* Most extra directories one configuration may name.
|
|
71
82
|
*
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-webdav-sync",
|
|
3
|
-
"version": "1.0.
|
|
4
|
-
"description": "用 WebDAV 在多台电脑之间同步 DSH
|
|
3
|
+
"version": "1.0.4",
|
|
4
|
+
"description": "用 WebDAV 在多台电脑之间同步 DSH 的配置:模型供应商与模型、已安装插件、其余 profile 配置、附加目录与 API 密钥。整个备份在离开本机前用口令加密(AES-256-GCM + PBKDF2),以分片三方合并的方式在服务端保留一个加密文件。",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"dsh",
|
|
7
7
|
"deepseek",
|