@goodandready/dsh-image-gen 0.11.13 → 0.11.14

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/lib/client.js CHANGED
@@ -287,6 +287,11 @@ window.__ModuleLoader__.load({
287
287
  { field: 'deliverAs', spec: selectField('deliverAs', DELIVERY_MODES), kind: 'select', options: DELIVERY_MODES, labelKey: 'f.deliverAs', hintKey: 'f.deliverAsHint', tab: 'general' },
288
288
  { field: 'outputDir', spec: textField('outputDir'), kind: 'text', labelKey: 'f.outputDir', hintKey: 'f.outputDirHint', placeholderKey: 'p.outputDir', tab: 'general' },
289
289
  { field: 'historyLimit', spec: numberField('historyLimit'), kind: 'number', labelKey: 'f.historyLimit', hintKey: 'f.historyLimitHint', placeholderKey: 'p.historyLimit', tab: 'general' },
290
+ { field: 'toolsetProfile', spec: selectField('toolsetProfile', ['minimal', 'all', 'custom']), kind: 'select', options: ['minimal', 'all', 'custom'], labelKey: 'f.toolsetProfile', hintKey: 'f.toolsetProfileHint', tab: 'general' },
291
+ { field: 'toolsetDesign', spec: booleanField('toolsetDesign'), kind: 'select', options: ['true', 'false'], labelKey: 'f.toolsetDesign', hintKey: 'f.toolsetDesignHint', tab: 'general' },
292
+ { field: 'toolsetProcessing', spec: booleanField('toolsetProcessing'), kind: 'select', options: ['true', 'false'], labelKey: 'f.toolsetProcessing', hintKey: 'f.toolsetProcessingHint', tab: 'general' },
293
+ { field: 'toolsetFrontend', spec: booleanField('toolsetFrontend'), kind: 'select', options: ['true', 'false'], labelKey: 'f.toolsetFrontend', hintKey: 'f.toolsetFrontendHint', tab: 'general' },
294
+ { field: 'toolsetCreative', spec: booleanField('toolsetCreative'), kind: 'select', options: ['true', 'false'], labelKey: 'f.toolsetCreative', hintKey: 'f.toolsetCreativeHint', tab: 'general' },
290
295
 
291
296
  // Provider: FAL
292
297
  { field: 'model', spec: textField('model'), kind: 'text', labelKey: 'f.model', hintKey: 'f.modelHint', placeholderKey: 'p.model', when: ['fal'], tab: 'provider' },
@@ -2458,6 +2463,16 @@ window.__ModuleLoader__.load({
2458
2463
  'p.outputDir': 'generated/images',
2459
2464
  'f.historyLimit': 'History Limit',
2460
2465
  'f.historyLimitHint': 'Maximum number of recent generation records kept in memory.',
2466
+ 'f.toolsetProfile': 'Toolset Profile',
2467
+ 'f.toolsetProfileHint': 'Select which tools to register into context: minimal (<= 3 core tools, saving ~8k prompt tokens), all (30 tools), or custom.',
2468
+ 'f.toolsetDesign': 'Design Toolset',
2469
+ 'f.toolsetDesignHint': 'Enable design tools: style_matrix, theme_pair, ui_asset, character_sheet, beautify_diagram, anchor.',
2470
+ 'f.toolsetProcessing': 'Processing Toolset',
2471
+ 'f.toolsetProcessingHint': 'Enable processing tools: remove_background, upscale, vectorize, convert, grid, smart_crop, asset_pack, replace_image_text.',
2472
+ 'f.toolsetFrontend': 'Frontend Toolset',
2473
+ 'f.toolsetFrontendHint': 'Enable frontend tools: extract_design_tokens, image_to_css_gradient, check_image_contrast, responsive_mockups.',
2474
+ 'f.toolsetCreative': 'Creative Toolset',
2475
+ 'f.toolsetCreativeHint': 'Enable creative tools: spritesheet, seamless_pattern, sketch_to_image, image_pack.',
2461
2476
  'p.historyLimit': '50',
2462
2477
 
2463
2478
  'f.model': 'FAL Model ID',
@@ -2724,6 +2739,16 @@ window.__ModuleLoader__.load({
2724
2739
  'p.outputDir': 'generated/images',
2725
2740
  'f.historyLimit': '历史记录数量',
2726
2741
  'f.historyLimitHint': '保存在内存历史列表中的最大条目数。',
2742
+ 'f.toolsetProfile': '工具集配置档',
2743
+ 'f.toolsetProfileHint': '选择注册到对话上下文的工具集:minimal(<= 3个核心工具,节省~8k提示词tokens),all(所有30个工具),或custom。',
2744
+ 'f.toolsetDesign': '设计工具集',
2745
+ 'f.toolsetDesignHint': '启用设计工具:style_matrix, theme_pair, ui_asset, character_sheet, beautify_diagram, anchor。',
2746
+ 'f.toolsetProcessing': '图像处理工具集',
2747
+ 'f.toolsetProcessingHint': '启用处理工具:remove_background, upscale, vectorize, convert, grid, smart_crop, asset_pack, replace_image_text。',
2748
+ 'f.toolsetFrontend': '前端工具集',
2749
+ 'f.toolsetFrontendHint': '启用前端工具:extract_design_tokens, image_to_css_gradient, check_image_contrast, responsive_mockups。',
2750
+ 'f.toolsetCreative': '创意工具集',
2751
+ 'f.toolsetCreativeHint': '启用创意工具:spritesheet, seamless_pattern, sketch_to_image, image_pack。',
2727
2752
  'p.historyLimit': '50',
2728
2753
 
2729
2754
  'f.model': 'FAL 模型 ID',
@@ -54,6 +54,40 @@ export const Config = z.object({
54
54
  .description('Default aspect ratio (square, landscape_16_9, portrait_9_16, etc.) used when prompt does not specify dimensions.')
55
55
  .default('square')
56
56
  .volatile(),
57
+ toolsetProfile: z
58
+ .string()
59
+ .description('Which toolset profile to register into conversation context: "minimal" (default, <= 3 core tools, saving ~8,000 prompt tokens), "all" (all 30 tools), or "custom".')
60
+ .default('minimal')
61
+ .volatile(),
62
+ toolsets: z
63
+ .object({
64
+ design: z.boolean().default(false).description('Design tools: style_matrix, theme_pair, ui_asset, character_sheet, beautify_diagram, anchor'),
65
+ processing: z.boolean().default(false).description('Processing tools: remove_background, upscale, vectorize, convert, grid, smart_crop, asset_pack, replace_image_text'),
66
+ frontend: z.boolean().default(false).description('Frontend tools: extract_design_tokens, image_to_css_gradient, check_image_contrast, responsive_mockups'),
67
+ creative: z.boolean().default(false).description('Creative tools: spritesheet, seamless_pattern, sketch_to_image, image_pack'),
68
+ })
69
+ .default({ design: false, processing: false, frontend: false, creative: false })
70
+ .volatile(),
71
+ toolsetDesign: z
72
+ .boolean()
73
+ .description('Enable design tools: style_matrix, theme_pair, ui_asset, character_sheet, beautify_diagram, anchor')
74
+ .default(false)
75
+ .volatile(),
76
+ toolsetProcessing: z
77
+ .boolean()
78
+ .description('Enable processing tools: remove_background, upscale, vectorize, convert, grid, smart_crop, asset_pack, replace_image_text')
79
+ .default(false)
80
+ .volatile(),
81
+ toolsetFrontend: z
82
+ .boolean()
83
+ .description('Enable frontend tools: extract_design_tokens, image_to_css_gradient, check_image_contrast, responsive_mockups')
84
+ .default(false)
85
+ .volatile(),
86
+ toolsetCreative: z
87
+ .boolean()
88
+ .description('Enable creative tools: spritesheet, seamless_pattern, sketch_to_image, image_pack')
89
+ .default(false)
90
+ .volatile(),
57
91
  deliverAs: z
58
92
  .string()
59
93
  .description('How generated images are returned: "link" keeps them on disk and embeds an inline preview link; "attachment" embeds full image bytes directly in the chat.')
@@ -1,5 +1,5 @@
1
- // register-tools.js — orchestrates all image-gen tool registrations (#216).
2
- // Each tool lives in lib/tools/* and is registered inside a labeled ctx.effect.
1
+ // register-tools.js — orchestrates all image-gen tool registrations (#216, #337).
2
+ // Supports 'minimal' (default: <= 3 core tools), 'all' (all 30 tools), and 'custom' (toggled toolsets).
3
3
 
4
4
  import { registerGenerationTools } from './tools/generation.js'
5
5
  import { registerProcessingTools } from './tools/processing-basic.js'
@@ -20,27 +20,53 @@ import { registerVisionOcrTools } from './tools/vision-ocr.js'
20
20
  import { registerDiagramTools } from './tools/diagram.js'
21
21
 
22
22
  /**
23
- * Registers every image-gen tool on the host tools service.
23
+ * Registers every image-gen tool on the host tools service according to toolsetProfile (#337).
24
24
  *
25
25
  * @param {object} ctx cordis context
26
26
  * @param {object} deps shared runtime from apply()
27
27
  */
28
28
  export function registerAllTools(ctx, deps) {
29
- registerGenerationTools(ctx, deps)
30
- registerProcessingTools(ctx, deps)
31
- registerProcessingAdvancedTools(ctx, deps)
32
- registerEditingTools(ctx, deps)
33
- registerInspectTools(ctx, deps)
34
- registerFrontendTools(ctx, deps)
35
- registerSketchTools(ctx, deps)
36
- registerSpritesheetTools(ctx, deps)
37
- registerPatternTools(ctx, deps)
38
- registerResponsiveTools(ctx, deps)
39
- registerAnchorTools(ctx, deps)
40
- registerUiAssetTools(ctx, deps)
41
- registerStyleMatrixTools(ctx, deps)
42
- registerThemePairTools(ctx, deps)
43
- registerCharacterSheetTools(ctx, deps)
44
- registerVisionOcrTools(ctx, deps)
45
- registerDiagramTools(ctx, deps)
29
+ const cfg = typeof deps.live === 'function' ? deps.live() : (deps.config || {})
30
+ const profile = cfg.toolsetProfile || 'minimal'
31
+ const isAll = profile === 'all'
32
+ const sets = cfg.toolsets || {}
33
+ const design = isAll || Boolean(sets.design || cfg.toolsetDesign)
34
+ const processing = isAll || Boolean(sets.processing || cfg.toolsetProcessing)
35
+ const frontend = isAll || Boolean(sets.frontend || cfg.toolsetFrontend)
36
+ const creative = isAll || Boolean(sets.creative || cfg.toolsetCreative)
37
+
38
+ // 1. Core tools: generate_image, edit_image, inspect_image_quality (<= 3 tools in minimal)
39
+ registerGenerationTools(ctx, deps, { coreOnly: !creative })
40
+ registerEditingTools(ctx, deps, { coreOnly: !processing })
41
+ registerInspectTools(ctx, deps, { coreOnly: !processing })
42
+
43
+ // 2. Processing toolset
44
+ if (processing) {
45
+ registerProcessingTools(ctx, deps)
46
+ registerProcessingAdvancedTools(ctx, deps)
47
+ registerVisionOcrTools(ctx, deps)
48
+ }
49
+
50
+ // 3. Design toolset
51
+ if (design) {
52
+ registerAnchorTools(ctx, deps)
53
+ registerUiAssetTools(ctx, deps)
54
+ registerStyleMatrixTools(ctx, deps)
55
+ registerThemePairTools(ctx, deps)
56
+ registerCharacterSheetTools(ctx, deps)
57
+ registerDiagramTools(ctx, deps)
58
+ }
59
+
60
+ // 4. Frontend toolset
61
+ if (frontend) {
62
+ registerFrontendTools(ctx, deps)
63
+ registerResponsiveTools(ctx, deps)
64
+ }
65
+
66
+ // 5. Creative toolset
67
+ if (creative) {
68
+ registerSketchTools(ctx, deps)
69
+ registerSpritesheetTools(ctx, deps)
70
+ registerPatternTools(ctx, deps)
71
+ }
46
72
  }
@@ -16,7 +16,7 @@ import { trackAndAssertLoopGuard } from '../loop-guard.js'
16
16
  import { sanitizeErrorAndLogs } from '../security.js'
17
17
 
18
18
 
19
- export function registerEditingTools(ctx, deps) {
19
+ export function registerEditingTools(ctx, deps, options = {}) {
20
20
  const {
21
21
  config,
22
22
  live,
@@ -29,41 +29,36 @@ export function registerEditingTools(ctx, deps) {
29
29
  ctx.tools.register(
30
30
  defineTool({
31
31
  name: 'edit_image',
32
- description:
33
- 'Edit an existing image using inpainting, targeted modification, or object replacement. '
34
- + 'Describe the desired changes in prompt. '
35
- + 'If image is omitted or "latest", automatically resolves the most recent image from the conversation session (#144). '
36
- + 'Leverages @goodandready/dsh-vision-bridge for pre-edit composition analysis (#145). '
37
- + 'Optionally accepts mask (path or attachment id) and strength (0.1-1.0). Returns safe dual-output (#150).',
32
+ description: 'Edit an image using inpainting, targeted modification, or object replacement. If image is omitted, uses the latest image from conversation.',
38
33
  parameters: {
39
34
  prompt: {
40
35
  type: 'string',
41
36
  required: true,
42
- description: 'Detailed description of the modification or edit to perform on the image.',
37
+ description: 'Description of modification or edit to perform.',
43
38
  },
44
39
  image: {
45
40
  type: 'string',
46
- description: 'Path or attachment id of the source image. Defaults to "latest" to auto-resolve from session history.',
41
+ description: 'Path or attachment ID of source image. Defaults to "latest".',
47
42
  },
48
43
  mask: {
49
44
  type: 'string',
45
+ description: 'Optional inpaint mask path or attachment ID.',
50
46
  },
51
47
  mask_image: {
52
48
  type: 'string',
53
- description: 'Optional path or attachment id of the inpaint mask image (alias for mask).',
54
- description: 'Optional path or attachment id of the inpaint mask image.',
49
+ description: 'Alias for mask.',
55
50
  },
56
51
  strength: {
57
52
  type: 'number',
58
- description: 'Denoising / edit strength between 0.1 and 1.0 (default 0.75 for edits, 0.9 for mask inpainting).',
53
+ description: 'Edit strength between 0.1 and 1.0 (default 0.75).',
59
54
  },
60
55
  output_name: {
61
56
  type: 'string',
62
- description: 'Optional file name stem for the saved edited image.',
57
+ description: 'Optional file name stem for edited image.',
63
58
  },
64
59
  output_dir: {
65
60
  type: 'string',
66
- description: 'Optional custom directory to write edited image into.',
61
+ description: 'Optional custom directory for edited image.',
67
62
  },
68
63
  },
69
64
  output: {
@@ -165,6 +160,7 @@ export function registerEditingTools(ctx, deps) {
165
160
  }),
166
161
  )
167
162
  }, 'dsh-image-gen: tool edit_image')
163
+ if (!options.coreOnly) {
168
164
  ctx.effect(() => {
169
165
  ctx.tools.register(
170
166
  defineTool({
@@ -450,5 +446,5 @@ export function registerEditingTools(ctx, deps) {
450
446
  }),
451
447
  )
452
448
  }, 'dsh-image-gen: tool remix_image')
453
-
449
+ }
454
450
  }
@@ -52,7 +52,7 @@ async function asyncPool(tasks, concurrency = 3) {
52
52
  return results
53
53
  }
54
54
 
55
- export function registerGenerationTools(ctx, deps) {
55
+ export function registerGenerationTools(ctx, deps, options = {}) {
56
56
  const {
57
57
  config,
58
58
  live,
@@ -64,17 +64,12 @@ export function registerGenerationTools(ctx, deps) {
64
64
  ctx.tools.register(
65
65
  defineTool({
66
66
  name: 'generate_image',
67
- description:
68
- 'Generate an image with the configured image provider. '
69
- + 'Saves the image to the session workspace and shows it in the conversation. '
70
- + 'Depending on how the deployment is configured the result carries either the image itself or a link to it; '
71
- + 'when you only get a link, answer from the prompt and the link rather than claiming to see the picture. '
72
- + 'Use for any text-to-image request. Pass count (1-4) to generate several variations in one call; cost scales with count (default 1).',
67
+ description: 'Generate an image from prompt. Saves the image to workspace and conversation. Pass count (1-4) for variations.',
73
68
  parameters: {
74
69
  prompt: {
75
70
  type: 'string',
76
71
  required: true,
77
- description: 'Detailed description of the image to generate (subject, style, lighting, composition, colors).',
72
+ description: 'Detailed description of the image to generate.',
78
73
  },
79
74
  image_size: {
80
75
  type: 'string',
@@ -83,7 +78,7 @@ export function registerGenerationTools(ctx, deps) {
83
78
  aspect_ratio: {
84
79
  type: 'string',
85
80
  enum: ['1:1', '16:9', '9:16', '4:3', '3:4', '3:2', '2:3'],
86
- description: 'Optional aspect ratio; overrides image_size when both are given. Sent as width/height to providers that accept pixels.',
81
+ description: 'Optional aspect ratio (e.g. 16:9, 1:1), overrides image_size.',
87
82
  },
88
83
  seed: {
89
84
  type: 'integer',
@@ -96,11 +91,11 @@ export function registerGenerationTools(ctx, deps) {
96
91
  },
97
92
  output_name: {
98
93
  type: 'string',
99
- description: 'Optional file name stem for the saved image (defaults to a slug of the prompt).',
94
+ description: 'Optional file name stem for the saved image.',
100
95
  },
101
96
  output_dir: {
102
97
  type: 'string',
103
- description: 'Optional custom directory to write images into (relative to session cwd or absolute). Defaults to outputDir from settings.',
98
+ description: 'Optional custom directory to write images into.',
104
99
  },
105
100
  style_preset: {
106
101
  type: 'string',
@@ -109,11 +104,11 @@ export function registerGenerationTools(ctx, deps) {
109
104
  palette_colors: {
110
105
  type: 'array',
111
106
  items: { type: 'string' },
112
- description: 'Optional array of hex color codes (#1e1b4b) to constrain and harmonize the color palette.',
107
+ description: 'Optional hex color codes (#1e1b4b) to harmonize palette.',
113
108
  },
114
109
  count: {
115
110
  type: 'integer',
116
- description: 'Number of variations to generate in one call, 1-4 (default 1). Cost scales with count.',
111
+ description: 'Variations count (1-4, default 1).',
117
112
  },
118
113
  prompts: {
119
114
  type: 'array',
@@ -123,60 +118,60 @@ export function registerGenerationTools(ctx, deps) {
123
118
  { type: 'object', additionalProperties: false, properties: { text: { type: 'string' } } },
124
119
  ],
125
120
  },
126
- description: 'Optional list of prompts (strings or {text}); one image is generated per prompt. When set, overrides count and the single prompt.',
121
+ description: 'Optional list of prompts for batch generation.',
127
122
  },
128
123
  negative_prompt: {
129
124
  type: 'string',
130
- description: 'Optional text describing what NOT to draw; sent to providers that support it (ignored otherwise).',
125
+ description: 'Optional text describing what NOT to draw.',
131
126
  },
132
127
  guidance_scale: {
133
128
  type: 'number',
134
- description: 'Optional prompt adherence (e.g. 1-20); sent to providers that support it (ignored otherwise).',
129
+ description: 'Optional prompt adherence (1-20).',
135
130
  },
136
131
  quality: {
137
132
  type: 'string',
138
133
  enum: ['auto', 'low', 'medium', 'high'],
139
- description: 'Optional quality for providers that support it (seedream, gemini). Ignored otherwise.',
134
+ description: 'Optional quality for supported providers.',
140
135
  },
141
136
  style: {
142
137
  type: 'string',
143
- description: 'Optional style preset for providers that support it (seedream, gemini). Ignored otherwise.',
138
+ description: 'Optional style preset for supported providers.',
144
139
  },
145
140
  source_image: {
146
141
  type: 'string',
147
- description: 'Optional path to an image file or an attachment id (sha256:...) to edit instead of drawing from scratch. Only providers that support image editing accept it; others refuse with a clear reason.',
142
+ description: 'Optional source image path or attachment ID for img2img.',
148
143
  },
149
144
  mask: {
150
145
  type: 'string',
151
- description: 'Optional path or attachment id of a mask image (same size as source) to restrict the edit to a region. Only used when source_image is set and the provider supports masks.',
146
+ description: 'Optional inpaint mask path or attachment ID.',
152
147
  },
153
148
  strength: {
154
149
  type: 'number',
155
- description: 'Optional edit strength (0-1) for providers that accept it; higher keeps more of the original.',
150
+ description: 'Optional edit strength (0-1).',
156
151
  },
157
152
  quality_gate: {
158
153
  type: 'boolean',
159
- description: 'Optional override to enable/disable automated quality gate and defect re-rolling for this call.',
154
+ description: 'Optional automated quality gate override.',
160
155
  },
161
156
  force: {
162
157
  type: 'boolean',
163
- description: 'Optional flag to bypass content-addressed cache and force a new API generation.',
158
+ description: 'Bypass cache and force new generation.',
164
159
  },
165
160
  reference_image: {
166
161
  type: 'string',
167
- description: 'Optional reference image (URL, path, or sha256:...) for character or style consistency.',
162
+ description: 'Optional reference image path or URL for consistency.',
168
163
  },
169
164
  reference_strength: {
170
165
  type: 'number',
171
- description: 'Optional strength of reference anchor influence (0.1 to 1.0).',
166
+ description: 'Strength of reference influence (0.1-1.0).',
172
167
  },
173
168
  face_reference: {
174
169
  type: 'string',
175
- description: 'Portrait reference image (URL, local path, or attachment id) for facial identity preservation via FaceID / IP-Adapter (#183).',
170
+ description: 'Portrait reference image for facial identity preservation.',
176
171
  },
177
172
  face_strength: {
178
173
  type: 'number',
179
- description: 'Facial identity similarity weight between 0.1 and 1.0 (default: 0.75).',
174
+ description: 'Facial identity similarity weight (0.1-1.0).',
180
175
  },
181
176
  },
182
177
  output: {
@@ -650,5 +645,7 @@ export function registerGenerationTools(ctx, deps) {
650
645
  }),
651
646
  )
652
647
  }, 'dsh-image-gen: tool generate_image')
653
- registerGenerationPackTool(ctx, deps)
648
+ if (!options.coreOnly) {
649
+ registerGenerationPackTool(ctx, deps)
650
+ }
654
651
  }
@@ -7,12 +7,13 @@ import {
7
7
  estimateSharpnessAndVariance,
8
8
  } from '../providers.js'
9
9
 
10
- export function registerInspectTools(ctx, deps) {
10
+ export function registerInspectTools(ctx, deps, options = {}) {
11
11
  const { resolveSource } = deps
12
- ctx.effect(() => {
13
- ctx.tools.register(
14
- defineTool({
15
- name: 'compare_images',
12
+ if (!options.coreOnly) {
13
+ ctx.effect(() => {
14
+ ctx.tools.register(
15
+ defineTool({
16
+ name: 'compare_images',
16
17
  description: 'Compare two images (paths or attachment ids) and report the fraction of differing pixels. Use to check how much an edit changed the original.',
17
18
  parameters: {
18
19
  image_a: { type: 'string', required: true, description: 'Path or attachment id of the first image.' },
@@ -37,6 +38,7 @@ export function registerInspectTools(ctx, deps) {
37
38
  }),
38
39
  )
39
40
  }, 'dsh-image-gen: tool compare_images')
41
+ }
40
42
  ctx.effect(() => {
41
43
  ctx.tools.register(
42
44
  defineTool({
package/lib/updater.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { spawn } from 'node:child_process'
2
- import { existsSync, readFileSync } from 'node:fs'
2
+ import { existsSync, readFileSync, unlinkSync, statSync } from 'node:fs'
3
3
  import { readFile } from 'node:fs/promises'
4
4
  import { homedir } from 'node:os'
5
5
  import { basename, dirname, isAbsolute, resolve } from 'node:path'
@@ -10,6 +10,60 @@ import { isLoopbackAddress, isPrivateLanAddress, extractHostName } from './secur
10
10
  * Implements the unified DSH plugin updater specification.
11
11
  */
12
12
 
13
+
14
+ export function isPidAlive(pid) {
15
+ if (!Number.isInteger(pid) || pid <= 0) return false
16
+ try {
17
+ process.kill(pid, 0)
18
+ return true
19
+ } catch (err) {
20
+ return err?.code === 'EPERM'
21
+ }
22
+ }
23
+
24
+ export function checkAndCleanProfileLock(profileDir) {
25
+ if (!profileDir) return { locked: false }
26
+ const lockPath = resolve(profileDir, 'package.json.lock')
27
+ if (!existsSync(lockPath)) return { locked: false }
28
+ let pid = null
29
+ try {
30
+ const raw = readFileSync(lockPath, 'utf8').trim()
31
+ try {
32
+ const parsed = JSON.parse(raw)
33
+ pid = parsed?.pid ?? Number(raw)
34
+ } catch (_e) {
35
+ pid = parseInt(raw, 10)
36
+ }
37
+ } catch (_err) {
38
+ /* lock unreadable */
39
+ }
40
+
41
+ if (Number.isInteger(pid) && pid > 0) {
42
+ if (isPidAlive(pid)) {
43
+ return { locked: true, pid }
44
+ }
45
+ // Stale lock: PID is not running
46
+ try {
47
+ unlinkSync(lockPath)
48
+ } catch (_err) {
49
+ /* non-fatal */
50
+ }
51
+ return { locked: false, staleCleaned: true, pid }
52
+ }
53
+
54
+ // Lockfile without valid PID: check age (> 10 min)
55
+ try {
56
+ const stat = statSync(lockPath)
57
+ if (Date.now() - stat.mtimeMs > 10 * 60_000) {
58
+ unlinkSync(lockPath)
59
+ return { locked: false, staleCleaned: true }
60
+ }
61
+ return { locked: true, pid: null }
62
+ } catch (_err) {
63
+ return { locked: false }
64
+ }
65
+ }
66
+
13
67
  const UPDATE_HEADER = 'x-dsh-plugin-update'
14
68
  const UPDATE_TIMEOUT_MS = 10 * 60_000
15
69
  const VERSION_CACHE_MS = 5 * 60_000
@@ -213,7 +267,6 @@ async function installExact(target, packageSpec, options) {
213
267
  '--profile',
214
268
  target.profileName,
215
269
  'add',
216
- '--config.minimumReleaseAge=0',
217
270
  packageSpec,
218
271
  `--registry=${options.registry ?? 'https://registry.npmjs.org/'}`,
219
272
  ],
@@ -232,9 +285,15 @@ async function installExact(target, packageSpec, options) {
232
285
  detail = (detail + String(chunk)).slice(-4000)
233
286
  })
234
287
  const timer = setTimeout(() => {
235
- child.kill()
288
+ try {
289
+ child.kill('SIGTERM')
290
+ setTimeout(() => {
291
+ try { child.kill('SIGKILL') } catch (_err) {}
292
+ checkAndCleanProfileLock(target.profileDir)
293
+ }, 1500)
294
+ } catch (_err) {}
236
295
  reject(new Error('Update timed out; use the normal DSH update flow.'))
237
- }, UPDATE_TIMEOUT_MS)
296
+ }, options.timeoutMs ?? UPDATE_TIMEOUT_MS)
238
297
  child.once('error', (error) => {
239
298
  clearTimeout(timer)
240
299
  reject(error)
@@ -290,6 +349,11 @@ export function registerPluginUpdater(ctx, options) {
290
349
  json(response, 409, { error: 'This plugin is already updating.' })
291
350
  return
292
351
  }
352
+ const lockState = checkAndCleanProfileLock(target.profileDir)
353
+ if (lockState.locked) {
354
+ json(response, 409, { error: `Another package installation is currently in progress${lockState.pid ? ` (PID ${lockState.pid})` : ''}.` })
355
+ return
356
+ }
293
357
  installing = true
294
358
  try {
295
359
  const before = await status(options, target)
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@goodandready/dsh-image-gen",
3
- "version": "0.11.13",
4
- "description": "Image generation for DeepSeek Harness: a generate_image tool with pluggable providers — the FAL queue, any OpenAI-compatible images API, or a ChatGPT/Grok subscription with no API key at all. The picture is shown inline in the conversation; the model receives either a link (works with any chat model) or the image itself (needs dsh-vision-bridge or a vision-capable model).",
3
+ "version": "0.11.14",
4
+ "description": "Image generation for DeepSeek Harness: a generate_image tool with pluggable providers \u2014 the FAL queue, any OpenAI-compatible images API, or a ChatGPT/Grok subscription with no API key at all. The picture is shown inline in the conversation; the model receives either a link (works with any chat model) or the image itself (needs dsh-vision-bridge or a vision-capable model).",
5
5
  "keywords": [
6
6
  "deepseek-harness",
7
7
  "dsh",
@@ -69,7 +69,7 @@
69
69
  "@deepseek-ai/schemastery": "^3.18.1"
70
70
  },
71
71
  "scripts": {
72
- "test": "node scripts/build-client.mjs --check && node --check lib/client.js && node --test test/*.test.mjs",
72
+ "test": "node scripts/build-client.mjs --check && node --check lib/client.js && node --import ./test/_setup.mjs --test test/*.test.mjs",
73
73
  "build:client": "node scripts/build-client.mjs",
74
74
  "publish:github": "bash scripts/publish-github.sh",
75
75
  "build:client:check": "node scripts/build-client.mjs --check"