dsh-perm-gate 4.1.2 → 4.2.0-beta.1
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/CHANGELOG.ja.md +0 -26
- package/CHANGELOG.ko.md +0 -24
- package/CHANGELOG.md +0 -26
- package/INSTALL.ja.md +14 -28
- package/INSTALL.ko.md +11 -23
- package/INSTALL.md +16 -32
- package/INSTALL.zh.md +11 -22
- package/README.ja.md +7 -34
- package/README.ko.md +7 -34
- package/README.md +7 -34
- package/README.zh.md +7 -29
- package/cordis.patch.yml +8 -11
- package/dsh.plugin.json +2 -2
- package/lib/client.js +8 -8
- package/lib/client.js.map +1 -1
- package/lib/config.d.ts +120 -1
- package/lib/config.js +71 -35
- package/lib/index.js +42 -77
- package/package.json +21 -19
- package/scripts/patch-permission-glyph.mjs +42 -225
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
|
-
*
|
|
4
|
-
* draw for them.
|
|
3
|
+
* Add `permissive-full` to the DSH composer's permission-glyph map.
|
|
5
4
|
*
|
|
6
5
|
* WHY THIS EXISTS
|
|
7
6
|
* ---------------
|
|
@@ -10,40 +9,22 @@
|
|
|
10
9
|
*
|
|
11
10
|
* const permissionGlyphs = new Map([
|
|
12
11
|
* ["read-only", <svg …/>],
|
|
12
|
+
* ["permissive", <svg …/>], // the 自动审查 tier's shield+eye
|
|
13
13
|
* ["workspace-write", <svg …/>],
|
|
14
|
-
* [
|
|
14
|
+
* ["danger-full-access", <svg …/>],
|
|
15
15
|
* ]);
|
|
16
16
|
* function permissionGlyph(value) { return permissionGlyphs.get(value); }
|
|
17
17
|
*
|
|
18
18
|
* The source comment states the rule: "host-configured names outside the design
|
|
19
19
|
* set get none." There is no plugin-facing seam — the option objects the host
|
|
20
20
|
* supplies carry only `{value, name, description}`, and the map itself is a
|
|
21
|
-
* closure variable. So a plugin-contributed tier
|
|
22
|
-
* dropdown and falls back to plain text in the
|
|
21
|
+
* closure variable. So a plugin-contributed tier such as `permissive-full`
|
|
22
|
+
* renders with no icon in the dropdown and falls back to plain text in the
|
|
23
|
+
* trigger, while the built-in `permissive` tier shows the shield+eye.
|
|
23
24
|
*
|
|
24
|
-
* The only way to give our
|
|
25
|
-
* This script does exactly that
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
* permissive sandbox: workspace-write <- the ["workspace-write",] entry
|
|
29
|
-
* permissive-full sandbox: danger-full-access <- the [FULL_ACCESS,] entry
|
|
30
|
-
*
|
|
31
|
-
* WHY THE SOURCES ARE DECLARED AND THE ANCHOR IS RESOLVED
|
|
32
|
-
* -------------------------------------------------------
|
|
33
|
-
* DSH 0.1.2 shipped four glyphs, `permissive` among them; 0.1.5-rc.2 ships three
|
|
34
|
-
* (`read-only` / `workspace-write` / `danger-full-access`) and the string
|
|
35
|
-
* `permissive` no longer occurs anywhere in the bundle — so a splice anchored on
|
|
36
|
-
* `["permissive",` dies with "anchor not found" on the current host. Two
|
|
37
|
-
* properties keep this working across such a design-set change:
|
|
38
|
-
*
|
|
39
|
-
* 1. the source key is a declaration (`GLYPH_TARGETS`) rather than a literal
|
|
40
|
-
* buried in the splice, so re-pointing a tier is a one-line edit; and
|
|
41
|
-
* 2. the anchor for a key is RESOLVED, not assumed to be a string literal:
|
|
42
|
-
* `["<key>",` first, then the constant alias the bundle may use instead
|
|
43
|
-
* (`[FULL_ACCESS,` for `"danger-full-access"`).
|
|
44
|
-
*
|
|
45
|
-
* When a declared source key is gone, the refusal names the keys the map DOES
|
|
46
|
-
* carry, so the next drift is diagnosed from the error text alone.
|
|
25
|
+
* The only way to give our tier the same glyph is to add an entry to that map.
|
|
26
|
+
* This script does exactly that: it duplicates the `permissive` entry under the
|
|
27
|
+
* `permissive-full` key, so the two tiers of one plugin look alike.
|
|
47
28
|
*
|
|
48
29
|
* VOLUNTARY, NOT AUTOMATIC
|
|
49
30
|
* ------------------------
|
|
@@ -61,11 +42,8 @@
|
|
|
61
42
|
* npx dsh-perm-gate-patch-glyph --check # report only; exit 1 if missing
|
|
62
43
|
* node scripts/patch-permission-glyph.mjs <path/to/client.js>
|
|
63
44
|
*
|
|
64
|
-
* With no argument it probes the
|
|
65
|
-
*
|
|
66
|
-
* both, only one of them serves the UI, so the explicit-path form is the answer
|
|
67
|
-
* when the probe picks the wrong one. The script is idempotent (a second run is a
|
|
68
|
-
* no-op), backs the file up once, applies every target or none, and refuses to
|
|
45
|
+
* With no argument it probes the usual global-install locations. The script is
|
|
46
|
+
* idempotent (a second run is a no-op), backs the file up once, and refuses to
|
|
69
47
|
* write a bundle it cannot slice correctly, so it can never half-apply.
|
|
70
48
|
*/
|
|
71
49
|
import { existsSync, readFileSync, writeFileSync, copyFileSync } from 'node:fs'
|
|
@@ -74,23 +52,13 @@ import { homedir } from 'node:os'
|
|
|
74
52
|
import { dirname, join, resolve } from 'node:path'
|
|
75
53
|
import { pathToFileURL } from 'node:url'
|
|
76
54
|
|
|
55
|
+
/** The map key whose entry is cloned, and the key it is cloned onto. */
|
|
56
|
+
export const SOURCE_KEY = 'permissive'
|
|
57
|
+
export const TARGET_KEY = 'permissive-full'
|
|
58
|
+
|
|
77
59
|
/** Relative path from a package root to the bundle holding the map. */
|
|
78
60
|
const BUNDLE = join('@deepseek-ai', 'dsh-client-ui-conversation', 'lib', 'client.js')
|
|
79
61
|
|
|
80
|
-
/**
|
|
81
|
-
* The glyphs to add: each plugin tier, and the built-in tier it copies.
|
|
82
|
-
*
|
|
83
|
-
* The pairing is by file-sandbox meaning, not by name similarity — `permissive`
|
|
84
|
-
* runs in the same sandbox as `workspace-write`, `permissive-full` in the same
|
|
85
|
-
* sandbox as `danger-full-access` — so the icon a user sees matches the access
|
|
86
|
-
* the tier actually grants. A tier listed here gets that built-in's entry
|
|
87
|
-
* verbatim apart from its key.
|
|
88
|
-
*/
|
|
89
|
-
export const GLYPH_TARGETS = [
|
|
90
|
-
{ target: 'permissive', source: 'workspace-write' },
|
|
91
|
-
{ target: 'permissive-full', source: 'danger-full-access' },
|
|
92
|
-
]
|
|
93
|
-
|
|
94
62
|
/** Thrown for every refusal; the CLI turns it into an exit code 1. */
|
|
95
63
|
export class GlyphPatchError extends Error {}
|
|
96
64
|
|
|
@@ -98,15 +66,6 @@ export class GlyphPatchError extends Error {}
|
|
|
98
66
|
* Candidate bundle paths, most specific first. Pure: no filesystem access, so
|
|
99
67
|
* the probe order is unit-testable.
|
|
100
68
|
*
|
|
101
|
-
* Profile scope before CLI-install scope, and that order is load-bearing on a
|
|
102
|
-
* machine with more than one DSH: a profile's UI is served by the profile's own
|
|
103
|
-
* dependency scope, not by whichever global CLI install happens to sit beside the
|
|
104
|
-
* running `node`. Measured on this project's own host — a global install under
|
|
105
|
-
* `dirname(node)` existed and shadowed the profile's bundle under an older probe
|
|
106
|
-
* order, so the patch reported success while the picker stayed icon-free. On a
|
|
107
|
-
* stock single-install machine every candidate is one inode, so the order decides
|
|
108
|
-
* nothing there; it only matters exactly when guessing wrong is silent.
|
|
109
|
-
*
|
|
110
69
|
* `execDir` is `dirname(process.execPath)`. Deriving from the running node
|
|
111
70
|
* binary — never from a hardcoded nvm version or install path — is what keeps
|
|
112
71
|
* the script working across node upgrades and re-pointed install symlinks.
|
|
@@ -116,111 +75,19 @@ export class GlyphPatchError extends Error {}
|
|
|
116
75
|
*/
|
|
117
76
|
export function candidateBundles({ execDir, cwd, dshHome }) {
|
|
118
77
|
return [
|
|
119
|
-
// 1.
|
|
120
|
-
join(cwd, 'node_modules', BUNDLE),
|
|
121
|
-
// 2. The hoisted scope every profile under DSH_HOME shares.
|
|
122
|
-
join(dshHome, 'profiles', 'node_modules', BUNDLE),
|
|
123
|
-
// 3. `@deepseek-ai` scope hoisted beside the running node binary.
|
|
78
|
+
// 1. `@deepseek-ai` scope hoisted beside the running node binary.
|
|
124
79
|
join(execDir, 'node_modules', BUNDLE),
|
|
125
|
-
//
|
|
80
|
+
// 2. The same scope nested inside the `dsh` package itself.
|
|
126
81
|
join(execDir, 'node_modules', '@deepseek-ai', 'dsh', 'node_modules', BUNDLE),
|
|
82
|
+
// 3. Whatever profile the caller is standing in.
|
|
83
|
+
join(cwd, 'node_modules', BUNDLE),
|
|
84
|
+
// 4. The hoisted scope shared by every profile under DSH_HOME.
|
|
85
|
+
join(dshHome, 'profiles', 'node_modules', BUNDLE),
|
|
127
86
|
]
|
|
128
87
|
}
|
|
129
88
|
|
|
130
89
|
/**
|
|
131
|
-
*
|
|
132
|
-
* name -> value. First declaration wins, so a later reassignment cannot shadow
|
|
133
|
-
* the spelling the map was built with above it.
|
|
134
|
-
*
|
|
135
|
-
* The bundle may key a map entry by such a name rather than by a literal, which
|
|
136
|
-
* is the whole reason the anchor is resolved instead of concatenated.
|
|
137
|
-
*
|
|
138
|
-
* @param {string} source
|
|
139
|
-
* @returns {Map<string, string>} constant name -> its string value
|
|
140
|
-
*/
|
|
141
|
-
export function pageConstants(source) {
|
|
142
|
-
const constants = new Map()
|
|
143
|
-
for (const match of source.matchAll(/(?:const|let|var)\s+([A-Za-z_$][\w$]*)\s*=\s*"([^"]*)"/g)) {
|
|
144
|
-
if (!constants.has(match[1])) constants.set(match[1], match[2])
|
|
145
|
-
}
|
|
146
|
-
return constants
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
/**
|
|
150
|
-
* The `permissionGlyphs` map's text, brackets included.
|
|
151
|
-
*
|
|
152
|
-
* Parsed rather than assumed: the map is a `new Map([ … ])` closure variable, so
|
|
153
|
-
* its extent is found by bracket depth from its `[`. Reading the map (instead of
|
|
154
|
-
* only probing for one anchor inside it) is what lets a refusal report the keys
|
|
155
|
-
* the host actually renders.
|
|
156
|
-
*
|
|
157
|
-
* @param {string} source
|
|
158
|
-
* @returns {string} the map array text, from `[` to the matching `]`
|
|
159
|
-
*/
|
|
160
|
-
export function glyphMapText(source) {
|
|
161
|
-
const start = source.search(/permissionGlyphs\s*=\s*new Map\(\s*\[/)
|
|
162
|
-
if (start === -1) throw new GlyphPatchError('permissionGlyphs map not found in this bundle')
|
|
163
|
-
const open = source.indexOf('[', start)
|
|
164
|
-
let depth = 0
|
|
165
|
-
for (let i = open; i < source.length; i += 1) {
|
|
166
|
-
const ch = source[i]
|
|
167
|
-
if (ch === '[' || ch === '(' || ch === '{') depth += 1
|
|
168
|
-
else if (ch === ']' || ch === ')' || ch === '}') {
|
|
169
|
-
depth -= 1
|
|
170
|
-
if (depth === 0) return source.slice(open, i + 1)
|
|
171
|
-
}
|
|
172
|
-
}
|
|
173
|
-
throw new GlyphPatchError('unterminated permissionGlyphs map')
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
/**
|
|
177
|
-
* The preset values the map renders a glyph for — literals as-is, and entries
|
|
178
|
-
* keyed by an in-page constant resolved to that constant's value.
|
|
179
|
-
*
|
|
180
|
-
* @param {string} source
|
|
181
|
-
* @returns {string[]} e.g. `['read-only', 'workspace-write', 'danger-full-access']`
|
|
182
|
-
*/
|
|
183
|
-
export function glyphKeys(source) {
|
|
184
|
-
const constants = pageConstants(source)
|
|
185
|
-
const keys = []
|
|
186
|
-
for (const match of glyphMapText(source).matchAll(/\[\s*(?:"([^"]+)"|([A-Za-z_$][\w$]*))\s*,/g)) {
|
|
187
|
-
if (match[1] !== undefined) keys.push(match[1])
|
|
188
|
-
else keys.push(constants.get(match[2]) ?? match[2])
|
|
189
|
-
}
|
|
190
|
-
return keys
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
/**
|
|
194
|
-
* Resolve the anchor text of one map entry, literal first, constant alias second.
|
|
195
|
-
*
|
|
196
|
-
* @param {string} source the bundle's text
|
|
197
|
-
* @param {string} key the preset value, e.g. `danger-full-access`
|
|
198
|
-
* @returns {{ anchor: string, index: number }}
|
|
199
|
-
* @throws {GlyphPatchError} when the host renders no glyph for `key`
|
|
200
|
-
*/
|
|
201
|
-
export function resolveAnchor(source, key) {
|
|
202
|
-
for (const anchor of [`["${key}",`, `['${key}',`]) {
|
|
203
|
-
const index = source.indexOf(anchor)
|
|
204
|
-
if (index !== -1) return { anchor, index }
|
|
205
|
-
}
|
|
206
|
-
const name = [...pageConstants(source)].find(([, value]) => value === key)?.[0]
|
|
207
|
-
if (name !== undefined) {
|
|
208
|
-
const anchor = `[${name},`
|
|
209
|
-
const index = source.indexOf(anchor)
|
|
210
|
-
if (index !== -1) return { anchor, index }
|
|
211
|
-
throw new GlyphPatchError(
|
|
212
|
-
`anchor not found: ${anchor} (the alias for "${key}")`,
|
|
213
|
-
)
|
|
214
|
-
}
|
|
215
|
-
throw new GlyphPatchError(
|
|
216
|
-
`this host renders no glyph for "${key}" — the map carries: ` +
|
|
217
|
-
`${glyphKeys(source).map((k) => `"${k}"`).join(', ')}\n` +
|
|
218
|
-
' Re-point GLYPH_TARGETS at a key the map does carry.',
|
|
219
|
-
)
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
/**
|
|
223
|
-
* Extract the full `["<key>", …]` (or `[CONST, …]`) map entry, by bracket depth.
|
|
90
|
+
* Extract the full `["<key>", …]` map entry, by bracket depth.
|
|
224
91
|
*
|
|
225
92
|
* The scan starts ON the entry's opening `[` (the one immediately before the
|
|
226
93
|
* key), so depth returns to zero exactly at that entry's closing `]`. Starting
|
|
@@ -231,12 +98,12 @@ export function resolveAnchor(source, key) {
|
|
|
231
98
|
* contain no brackets.
|
|
232
99
|
*
|
|
233
100
|
* @param {string} source
|
|
234
|
-
* @param {string}
|
|
101
|
+
* @param {string} key
|
|
235
102
|
* @returns {string} the entry, brackets included
|
|
236
103
|
*/
|
|
237
|
-
export function sliceEntry(source,
|
|
238
|
-
const start = source.indexOf(
|
|
239
|
-
if (start === -1) throw new GlyphPatchError(`anchor not found: ${
|
|
104
|
+
export function sliceEntry(source, key) {
|
|
105
|
+
const start = source.indexOf(`["${key}",`)
|
|
106
|
+
if (start === -1) throw new GlyphPatchError(`anchor not found: ["${key}",`)
|
|
240
107
|
let depth = 0
|
|
241
108
|
for (let i = start; i < source.length; i += 1) {
|
|
242
109
|
const ch = source[i]
|
|
@@ -246,41 +113,24 @@ export function sliceEntry(source, anchor) {
|
|
|
246
113
|
if (depth === 0) return source.slice(start, i + 1)
|
|
247
114
|
}
|
|
248
115
|
}
|
|
249
|
-
throw new GlyphPatchError(`unterminated entry for ${
|
|
116
|
+
throw new GlyphPatchError(`unterminated entry for ["${key}",`)
|
|
250
117
|
}
|
|
251
118
|
|
|
252
119
|
/**
|
|
253
|
-
*
|
|
254
|
-
*
|
|
255
|
-
* All or nothing: a source key this host does not carry throws before the caller
|
|
256
|
-
* can write anything, so a partially-glyphed bundle is impossible. Idempotent
|
|
257
|
-
* per target — a target already present is left untouched and reported as such.
|
|
120
|
+
* Clone the `permissive` glyph entry onto the `permissive-full` key.
|
|
258
121
|
*
|
|
259
122
|
* @param {string} source the bundle's text
|
|
260
|
-
* @returns {{ changed: boolean, source: string,
|
|
123
|
+
* @returns {{ changed: boolean, source: string, entry: string | null }}
|
|
124
|
+
* `changed: false` means the target key was already present (idempotent no-op).
|
|
261
125
|
*/
|
|
262
126
|
export function applyGlyphPatch(source) {
|
|
263
|
-
|
|
264
|
-
const
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
for (const { target, source: sourceKey } of GLYPH_TARGETS) {
|
|
268
|
-
if (next.includes(`["${target}",`)) {
|
|
269
|
-
already.push(target)
|
|
270
|
-
continue
|
|
271
|
-
}
|
|
272
|
-
const { anchor } = resolveAnchor(next, sourceKey)
|
|
273
|
-
const sourceEntry = sliceEntry(next, anchor)
|
|
274
|
-
if (!sourceEntry.includes('svg')) {
|
|
275
|
-
throw new GlyphPatchError(`the ${anchor} entry does not look like a glyph`)
|
|
276
|
-
}
|
|
277
|
-
const clonedEntry = sourceEntry.replace(anchor, `["${target}",`)
|
|
278
|
-
// A function replacement: the entry is spliced verbatim, `$` never expanded.
|
|
279
|
-
next = next.replace(sourceEntry, () => `${sourceEntry},\n\t\t\t${clonedEntry}`)
|
|
280
|
-
added.push(target)
|
|
127
|
+
if (source.includes(`["${TARGET_KEY}",`)) return { changed: false, source, entry: null }
|
|
128
|
+
const sourceEntry = sliceEntry(source, SOURCE_KEY)
|
|
129
|
+
if (!sourceEntry.includes('svg')) {
|
|
130
|
+
throw new GlyphPatchError(`the ["${SOURCE_KEY}",] entry does not look like a glyph`)
|
|
281
131
|
}
|
|
282
|
-
|
|
283
|
-
return { changed:
|
|
132
|
+
const clonedEntry = sourceEntry.replace(`["${SOURCE_KEY}",`, `["${TARGET_KEY}",`)
|
|
133
|
+
return { changed: true, source: source.replace(sourceEntry, `${sourceEntry},\n\t\t\t${clonedEntry}`), entry: clonedEntry }
|
|
284
134
|
}
|
|
285
135
|
|
|
286
136
|
/** Resolve the bundle to patch, or throw with an explanation. */
|
|
@@ -301,29 +151,8 @@ export function findBundle(argv, where) {
|
|
|
301
151
|
)
|
|
302
152
|
}
|
|
303
153
|
|
|
304
|
-
/**
|
|
305
|
-
* A warning block naming the other DSH installs that carry this bundle.
|
|
306
|
-
*
|
|
307
|
-
* Reported rather than patched: on a host with a global CLI install and a
|
|
308
|
-
* launcher-managed runtime, only one of them serves the UI in front of the user,
|
|
309
|
-
* and picking the wrong one is invisible — the patch succeeds and the picker
|
|
310
|
-
* stays icon-free. Naming the others turns that into a one-line fix (pass the
|
|
311
|
-
* path explicitly).
|
|
312
|
-
*
|
|
313
|
-
* @param {string[]} others bundle paths that exist but were not chosen
|
|
314
|
-
* @returns {string} an empty string when there is nothing to report
|
|
315
|
-
*/
|
|
316
|
-
function describeOtherInstalls(others) {
|
|
317
|
-
if (others.length === 0) return ''
|
|
318
|
-
return ` note: ${others.length} other DSH install(s) carry this bundle — NOT patched:\n` +
|
|
319
|
-
others.map((path) => ` ${path}\n`).join('') +
|
|
320
|
-
' If the tier still has no icon, the UI is served by one of those; re-run with\n' +
|
|
321
|
-
' that path as the argument.\n'
|
|
322
|
-
}
|
|
323
|
-
|
|
324
154
|
function main(argv) {
|
|
325
155
|
const check = argv.includes('--check')
|
|
326
|
-
const explicit = argv.slice(2).find((arg) => !arg.startsWith('-'))
|
|
327
156
|
const where = {
|
|
328
157
|
execDir: dirname(process.execPath),
|
|
329
158
|
cwd: process.cwd(),
|
|
@@ -331,27 +160,17 @@ function main(argv) {
|
|
|
331
160
|
}
|
|
332
161
|
|
|
333
162
|
const bundle = findBundle(argv, where)
|
|
334
|
-
const others = explicit === undefined
|
|
335
|
-
? candidateBundles(where).filter((candidate) => candidate !== bundle && existsSync(candidate))
|
|
336
|
-
: []
|
|
337
|
-
const otherNote = describeOtherInstalls(others)
|
|
338
163
|
const original = readFileSync(bundle, 'utf8')
|
|
339
|
-
const { changed, source: patched
|
|
164
|
+
const { changed, source: patched } = applyGlyphPatch(original)
|
|
340
165
|
|
|
341
166
|
if (!changed) {
|
|
342
|
-
process.stdout.write(
|
|
343
|
-
`patch-permission-glyph: all glyphs already present in\n ${bundle}\n` +
|
|
344
|
-
` present: ${already.map((key) => `["${key}",]`).join(' ')}\n` +
|
|
345
|
-
otherNote,
|
|
346
|
-
)
|
|
167
|
+
process.stdout.write(`patch-permission-glyph: ["${TARGET_KEY}",] already present in\n ${bundle}\n`)
|
|
347
168
|
return 0
|
|
348
169
|
}
|
|
349
170
|
if (check) {
|
|
350
171
|
process.stdout.write(
|
|
351
|
-
`patch-permission-glyph:
|
|
352
|
-
|
|
353
|
-
' (--check: nothing written; re-run without --check to apply)\n' +
|
|
354
|
-
otherNote,
|
|
172
|
+
`patch-permission-glyph: ["${TARGET_KEY}",] is MISSING from\n ${bundle}\n` +
|
|
173
|
+
' (--check: nothing written; re-run without --check to apply)\n',
|
|
355
174
|
)
|
|
356
175
|
return 1
|
|
357
176
|
}
|
|
@@ -370,11 +189,9 @@ function main(argv) {
|
|
|
370
189
|
}
|
|
371
190
|
|
|
372
191
|
process.stdout.write(
|
|
373
|
-
`patch-permission-glyph: added
|
|
374
|
-
already.map((key) => ` already present: ["${key}",]\n`).join('') +
|
|
192
|
+
`patch-permission-glyph: added ["${TARGET_KEY}",] to\n ${bundle}\n` +
|
|
375
193
|
` backup: ${backup}\n` +
|
|
376
|
-
' NOTE: this edits a DSH package — re-run after any DSH upgrade or reinstall.\n'
|
|
377
|
-
otherNote,
|
|
194
|
+
' NOTE: this edits a DSH package — re-run after any DSH upgrade or reinstall.\n',
|
|
378
195
|
)
|
|
379
196
|
return 0
|
|
380
197
|
}
|