@foggy-projects/deepseek-harness-plugin 0.4.0-beta.2 → 0.4.0-beta.3

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 CHANGED
@@ -8,7 +8,7 @@ only when the user selects **Initialize Foggy**.
8
8
  ## Local beta installation
9
9
 
10
10
  ```powershell
11
- dsh plugin --profile web add --workspace-root ./foggy-projects-deepseek-harness-plugin-0.4.0-beta.2.tgz
11
+ dsh plugin --profile web add --workspace-root ./foggy-projects-deepseek-harness-plugin-0.4.0-beta.3.tgz
12
12
  ```
13
13
 
14
14
  Restart `dsh web`, open Settings → Plugins → Foggy Data Analysis, and initialize
@@ -3,7 +3,7 @@ set -euo pipefail
3
3
 
4
4
  DSH_VERSION="0.1.1-rc.2"
5
5
  PNPM_VERSION="11.7.0"
6
- PLUGIN_VERSION="0.4.0-beta.2"
6
+ PLUGIN_VERSION="0.4.0-beta.3"
7
7
  PLUGIN_REF="v${PLUGIN_VERSION}"
8
8
  PLUGIN_REPOSITORY="https://github.com/foggy-projects/foggy-deepseek-harness-plugin.git"
9
9
 
package/lib/client.js CHANGED
@@ -52,6 +52,15 @@ window.__ModuleLoader__.load({
52
52
  start: '启动 Runtime',
53
53
  stop: '停止 Runtime',
54
54
  working: '操作进行中…',
55
+ progressWorking: '正在初始化 Foggy…',
56
+ progressPreflight: '检查运行环境',
57
+ progressCli: '准备 CLI',
58
+ progressLauncher: '准备 Launcher',
59
+ progressAnalysis: '准备分析 Skill',
60
+ progressSkills: '安装工作区 Skills',
61
+ progressState: '写入安装状态',
62
+ progressComplete: '初始化完成',
63
+ progressFiles: '文件',
55
64
  accepted: '操作已开始;页面会自动刷新状态。',
56
65
  actionFailed: '操作失败',
57
66
  roots: '本地目录',
@@ -90,6 +99,15 @@ window.__ModuleLoader__.load({
90
99
  start: 'Start Runtime',
91
100
  stop: 'Stop Runtime',
92
101
  working: 'Operation in progress…',
102
+ progressWorking: 'Initializing Foggy…',
103
+ progressPreflight: 'Checking prerequisites',
104
+ progressCli: 'Preparing CLI',
105
+ progressLauncher: 'Preparing Launcher',
106
+ progressAnalysis: 'Preparing analysis Skill',
107
+ progressSkills: 'Installing workspace Skills',
108
+ progressState: 'Writing install state',
109
+ progressComplete: 'Initialization complete',
110
+ progressFiles: 'files',
93
111
  accepted: 'The operation started; this page will refresh automatically.',
94
112
  actionFailed: 'Operation failed',
95
113
  roots: 'Local directories',
@@ -110,6 +128,7 @@ window.__ModuleLoader__.load({
110
128
  .foggy-state{display:flex;align-items:center;gap:8px;font-size:14px;font-weight:600}.foggy-dot{width:9px;height:9px;border-radius:50%;background:var(--dsw-alias-label-tertiary)}.foggy-dot[data-state=ready],.foggy-dot[data-state=running]{background:var(--dsw-alias-state-success-primary)}.foggy-dot[data-state=degraded]{background:var(--dsw-alias-state-error-primary)}
111
129
  .foggy-actions{display:flex;gap:8px;flex-wrap:wrap}.foggy-button{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-primary);font:inherit;border-radius:8px;padding:7px 12px;cursor:pointer}.foggy-button:hover{background:var(--dsw-alias-interactive-bg-hover)}.foggy-button:disabled{opacity:.55;cursor:not-allowed}.foggy-button-primary{border-color:var(--dsw-alias-state-business-primary);background:var(--dsw-alias-state-business-primary);color:white}
112
130
  .foggy-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:10px}.foggy-card{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);border-radius:10px;padding:13px 14px}.foggy-card-head{display:flex;justify-content:space-between;gap:8px}.foggy-card strong{font-size:13px}.foggy-badge{font-size:11px;color:var(--dsw-alias-label-secondary)}.foggy-card code{display:block;margin-top:8px;color:var(--dsw-alias-label-tertiary);font-size:11px;overflow-wrap:anywhere}
131
+ .foggy-progress{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);border-radius:10px;padding:13px 14px}.foggy-progress-head{display:flex;align-items:center;justify-content:space-between;gap:12px;font-size:13px}.foggy-progress-head strong{font-weight:600}.foggy-progress-percent{color:var(--dsw-alias-label-secondary);font-variant-numeric:tabular-nums}.foggy-progress-track{height:8px;margin-top:10px;border-radius:999px;background:var(--dsw-alias-bg-layer-1);overflow:hidden}.foggy-progress-fill{height:100%;border-radius:inherit;background:var(--dsw-alias-state-business-primary);transition:width .25s ease}.foggy-progress-meta{display:flex;justify-content:space-between;gap:12px;margin-top:8px;color:var(--dsw-alias-label-tertiary);font-size:11px;line-height:16px}.foggy-progress-file{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
113
132
  .foggy-message{margin:0;font-size:13px;line-height:20px;color:var(--dsw-alias-label-secondary)}.foggy-message[data-error=true]{color:var(--dsw-alias-state-error-primary)}.foggy-paths,.foggy-next{border-top:1px solid var(--dsw-alias-border-l2);padding-top:14px}.foggy-paths h4,.foggy-next h4{margin:0 0 8px;font-size:13px}.foggy-paths dl{display:grid;grid-template-columns:100px minmax(0,1fr);gap:6px 10px;margin:0}.foggy-paths dt{color:var(--dsw-alias-label-tertiary);font-size:12px}.foggy-paths dd{margin:0;min-width:0;overflow-wrap:anywhere;font-family:var(--ds-font-family-code);font-size:11px}.foggy-next p{margin:0;color:var(--dsw-alias-label-secondary);font-size:13px;line-height:20px}
114
133
  @media (width<=680px){.foggy-grid{grid-template-columns:minmax(0,1fr)}}
115
134
  `
@@ -146,6 +165,52 @@ window.__ModuleLoader__.load({
146
165
  })
147
166
  }
148
167
 
168
+ function progressPanel(progress, t) {
169
+ if (!progress) return null
170
+ const percent = Math.max(0, Math.min(100, Number(progress.percent) || 0))
171
+ const phaseKeys = {
172
+ preflight: 'progressPreflight',
173
+ cli: 'progressCli',
174
+ launcher: 'progressLauncher',
175
+ 'analysis-skill': 'progressAnalysis',
176
+ 'workspace-skills': 'progressSkills',
177
+ state: 'progressState',
178
+ complete: 'progressComplete',
179
+ }
180
+ const phase = t(phaseKeys[progress.phase] || 'progressWorking')
181
+ const step = progress.step?.index && progress.step?.total
182
+ ? `${progress.step.index}/${progress.step.total}`
183
+ : ''
184
+ const files = progress.files?.total
185
+ ? `${progress.files.completed || 0}/${progress.files.total} ${t('progressFiles')}`
186
+ : ''
187
+ return jsxs('div', {
188
+ className: 'foggy-progress',
189
+ role: 'status',
190
+ 'aria-live': 'polite',
191
+ children: [
192
+ jsxs('div', { className: 'foggy-progress-head', children: [
193
+ jsx('strong', { children: progress.message || phase }),
194
+ jsx('span', { className: 'foggy-progress-percent', children: `${percent}%` }),
195
+ ] }),
196
+ jsx('div', {
197
+ className: 'foggy-progress-track',
198
+ role: 'progressbar',
199
+ 'aria-valuemin': 0,
200
+ 'aria-valuemax': 100,
201
+ 'aria-valuenow': percent,
202
+ 'aria-label': phase,
203
+ children: jsx('div', { className: 'foggy-progress-fill', style: { width: `${percent}%` } }),
204
+ }),
205
+ jsxs('div', { className: 'foggy-progress-meta', children: [
206
+ jsx('span', { children: phase }),
207
+ jsx('span', { children: [step, files].filter(Boolean).join(' · ') }),
208
+ ] }),
209
+ progress.currentFile ? jsx('div', { className: 'foggy-progress-meta foggy-progress-file', title: progress.currentFile, children: progress.currentFile }) : null,
210
+ ],
211
+ })
212
+ }
213
+
149
214
  function FoggySettingsTab({ api, t }) {
150
215
  const [view, setView] = useState({ phase: 'loading', status: null, message: '', error: false })
151
216
 
@@ -188,6 +253,7 @@ window.__ModuleLoader__.load({
188
253
  const operationError = status.operation?.state === 'failed'
189
254
  ? status.operation.error || status.operation.result?.error?.message || t('actionFailed')
190
255
  : ''
256
+ const progress = busy ? progressPanel(status.operation?.progress, t) : null
191
257
  const stateLabels = { 'not-installed': 'notInstalled', degraded: 'degraded', ready: 'ready', running: 'running' }
192
258
  return jsxs('section', { className: 'foggy-settings', children: [
193
259
  jsxs('div', { className: 'foggy-hero', children: [
@@ -204,6 +270,7 @@ window.__ModuleLoader__.load({
204
270
  status.running ? jsx('button', { className: 'foggy-button', type: 'button', disabled: busy, onClick: () => run('runtimeStop'), children: t('stop') }) : null,
205
271
  ] }),
206
272
  ] }),
273
+ progress,
207
274
  busy || operationError || view.message ? jsx('p', { className: 'foggy-message', 'data-error': operationError || view.error ? 'true' : undefined, children: busy ? t('working') : operationError || view.message }) : null,
208
275
  jsxs('div', { className: 'foggy-grid', children: [
209
276
  componentCard(t('python'), status.components.python, 'tool', t),
package/lib/index.js CHANGED
@@ -46,6 +46,14 @@ async function readJson(path) {
46
46
  return JSON.parse(await readFile(path, 'utf8'))
47
47
  }
48
48
 
49
+ async function readOptionalJson(path) {
50
+ try {
51
+ return await readJson(path)
52
+ } catch {
53
+ return null
54
+ }
55
+ }
56
+
49
57
  async function commandVersion(command, args) {
50
58
  try {
51
59
  const { stdout, stderr } = await execFileAsync(command, args, {
@@ -123,6 +131,7 @@ export class FoggyIntegrationGateway extends TypertRemoteService {
123
131
  const roots = defaultRoots()
124
132
  const statePath = join(roots.installRoot, 'install-state.json')
125
133
  const runtimeStatePath = join(roots.dataRoot, 'runtime-state.json')
134
+ const progressPath = join(roots.dataRoot, 'operation-progress.json')
126
135
  const manifest = await readJson(versionsFile)
127
136
  let pythonProbe
128
137
  try {
@@ -156,6 +165,22 @@ export class FoggyIntegrationGateway extends TypertRemoteService {
156
165
  }
157
166
  const installed = components.cli.installed && components.launcher.installed && components.analysisSkill.installed
158
167
  const running = Boolean(runtime && processRunning(Number(runtime.pid)))
168
+ const progress = await readOptionalJson(progressPath)
169
+ let operation = operationView(this.operation)
170
+ if (operation.state === 'running' && progress?.operationId === operation.id) {
171
+ operation = { ...operation, progress }
172
+ } else if (!this.operation && progress?.state === 'running' && progress.operationId) {
173
+ operation = {
174
+ id: progress.operationId,
175
+ kind: progress.kind || 'initialize',
176
+ state: 'running',
177
+ startedAt: progress.startedAt || null,
178
+ finishedAt: null,
179
+ result: null,
180
+ error: null,
181
+ progress,
182
+ }
183
+ }
159
184
  return {
160
185
  success: true,
161
186
  packageVersion: manifest.packageVersion,
@@ -166,7 +191,7 @@ export class FoggyIntegrationGateway extends TypertRemoteService {
166
191
  roots,
167
192
  projectRoot: process.env.FOGGY_PROJECT_ROOT || process.cwd(),
168
193
  components,
169
- operation: operationView(this.operation),
194
+ operation,
170
195
  next: installed ? (running ? 'configure-database' : 'start-runtime') : 'initialize',
171
196
  }
172
197
  }
@@ -211,6 +236,10 @@ export class FoggyIntegrationGateway extends TypertRemoteService {
211
236
  }
212
237
  const id = `${Date.now()}-${Math.random().toString(16).slice(2, 10)}`
213
238
  const args = explicitArgs ?? ['install', '--project-root', process.env.FOGGY_PROJECT_ROOT || process.cwd()]
239
+ if (!explicitArgs) {
240
+ const roots = defaultRoots()
241
+ args.push('--progress-file', join(roots.dataRoot, 'operation-progress.json'), '--operation-id', id, '--operation-kind', kind)
242
+ }
214
243
  if (replaceSkill) args.push('--replace-skill')
215
244
  for (const cacheDir of (process.env.FOGGY_ASSET_CACHE_DIRS || '').split(delimiter).filter(Boolean)) {
216
245
  args.push('--asset-cache-dir', cacheDir)
@@ -223,6 +252,17 @@ export class FoggyIntegrationGateway extends TypertRemoteService {
223
252
  finishedAt: null,
224
253
  result: null,
225
254
  error: null,
255
+ progress: !explicitArgs ? {
256
+ schemaVersion: 'foggy-deepseek-onboarding-progress/v1',
257
+ operationId: id,
258
+ kind,
259
+ state: 'running',
260
+ phase: 'preflight',
261
+ message: 'Starting Foggy initialization',
262
+ currentFile: null,
263
+ percent: 0,
264
+ step: { index: 1, total: 6 },
265
+ } : null,
226
266
  promise: null,
227
267
  }
228
268
  operation.promise = runOnboarding(args)
@@ -230,11 +270,27 @@ export class FoggyIntegrationGateway extends TypertRemoteService {
230
270
  operation.state = result.success === false ? 'failed' : 'succeeded'
231
271
  operation.result = result
232
272
  operation.finishedAt = new Date().toISOString()
273
+ if (operation.progress) {
274
+ operation.progress = {
275
+ ...operation.progress,
276
+ state: operation.state,
277
+ phase: operation.state === 'succeeded' ? 'complete' : operation.progress.phase,
278
+ message: operation.state === 'succeeded' ? 'Foggy initialization completed' : 'Foggy initialization failed',
279
+ percent: operation.state === 'succeeded' ? 100 : operation.progress.percent,
280
+ }
281
+ }
233
282
  })
234
283
  .catch((error) => {
235
284
  operation.state = 'failed'
236
285
  operation.error = String(error.message ?? error)
237
286
  operation.finishedAt = new Date().toISOString()
287
+ if (operation.progress) {
288
+ operation.progress = {
289
+ ...operation.progress,
290
+ state: 'failed',
291
+ message: 'Foggy initialization failed',
292
+ }
293
+ }
238
294
  })
239
295
  this.operation = operation
240
296
  return { success: true, accepted: true, operation: operationView(operation) }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@foggy-projects/deepseek-harness-plugin",
3
- "version": "0.4.0-beta.2",
3
+ "version": "0.4.0-beta.3",
4
4
  "description": "Foggy Java data analysis engine integration for DeepSeek Harness",
5
5
  "type": "module",
6
6
  "main": "./lib/index.js",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "schemaVersion": "foggy-deepseek-onboarding-versions/v1",
3
- "packageVersion": "0.4.0-beta.2",
3
+ "packageVersion": "0.4.0-beta.3",
4
4
  "validatedAt": "2026-08-28",
5
5
  "components": {
6
6
  "deepseekHarness": {
@@ -38,6 +38,93 @@ class OnboardingError(RuntimeError):
38
38
  pass
39
39
 
40
40
 
41
+ PROGRESS_SCHEMA = "foggy-deepseek-onboarding-progress/v1"
42
+ ACTIVE_PROGRESS: "ProgressReporter | None" = None
43
+
44
+
45
+ class ProgressReporter:
46
+ total_steps = 6
47
+
48
+ def __init__(self, path: str | None, operation_id: str | None, kind: str) -> None:
49
+ self.path = Path(path).expanduser() if path else None
50
+ self.operation_id = operation_id
51
+ self.kind = kind
52
+ self.started_at = now_utc()
53
+ self.phase = "preflight"
54
+ self.step_index = 0
55
+ self.fraction = 0.0
56
+ self.message = ""
57
+ self.current_file: str | None = None
58
+ self.completed_files: int | None = None
59
+ self.total_files: int | None = None
60
+
61
+ @property
62
+ def enabled(self) -> bool:
63
+ return self.path is not None
64
+
65
+ def _write(self, state: str, *, finished_at: str | None = None, error: str | None = None) -> None:
66
+ if not self.path:
67
+ return
68
+ bounded_step = max(0, min(self.step_index, self.total_steps))
69
+ bounded_fraction = max(0.0, min(float(self.fraction), 1.0))
70
+ percent = 100 if state == "succeeded" else round(((bounded_step + bounded_fraction) / self.total_steps) * 100)
71
+ payload = {
72
+ "schemaVersion": PROGRESS_SCHEMA,
73
+ "operationId": self.operation_id,
74
+ "kind": self.kind,
75
+ "state": state,
76
+ "phase": self.phase,
77
+ "message": self.message,
78
+ "currentFile": self.current_file,
79
+ "percent": percent,
80
+ "step": {"index": min(bounded_step + 1, self.total_steps), "total": self.total_steps},
81
+ "startedAt": self.started_at,
82
+ "updatedAt": now_utc(),
83
+ }
84
+ if self.completed_files is not None and self.total_files is not None:
85
+ payload["files"] = {"completed": self.completed_files, "total": self.total_files}
86
+ if finished_at:
87
+ payload["finishedAt"] = finished_at
88
+ if error:
89
+ payload["error"] = error
90
+ self.path.parent.mkdir(parents=True, exist_ok=True)
91
+ atomic_json(self.path, payload)
92
+
93
+ def update(
94
+ self,
95
+ phase: str,
96
+ step_index: int,
97
+ message: str,
98
+ *,
99
+ fraction: float = 0.0,
100
+ current_file: str | None = None,
101
+ completed_files: int | None = None,
102
+ total_files: int | None = None,
103
+ ) -> None:
104
+ self.phase = phase
105
+ self.step_index = step_index
106
+ self.fraction = fraction
107
+ self.message = message
108
+ self.current_file = current_file
109
+ self.completed_files = completed_files
110
+ self.total_files = total_files
111
+ self._write("running")
112
+
113
+ def finish(self) -> None:
114
+ self.phase = "complete"
115
+ self.step_index = self.total_steps
116
+ self.fraction = 0.0
117
+ self.message = "Foggy initialization completed"
118
+ self.current_file = None
119
+ self.completed_files = None
120
+ self.total_files = None
121
+ self._write("succeeded", finished_at=now_utc())
122
+
123
+ def fail(self, error: object) -> None:
124
+ self.message = "Foggy initialization failed"
125
+ self._write("failed", finished_at=now_utc(), error=str(error))
126
+
127
+
41
128
  def now_utc() -> str:
42
129
  return dt.datetime.now(dt.timezone.utc).isoformat()
43
130
 
@@ -182,7 +269,18 @@ def cached_asset(name: str, expected: str, cache_dirs: list[Path]) -> Path | Non
182
269
  return None
183
270
 
184
271
 
185
- def materialize(asset: dict, destination: Path, cache_dirs: list[Path]) -> dict:
272
+ def materialize(
273
+ asset: dict,
274
+ destination: Path,
275
+ cache_dirs: list[Path],
276
+ *,
277
+ progress: ProgressReporter | None = None,
278
+ progress_phase: str = "",
279
+ progress_step: int = 0,
280
+ progress_index: int = 0,
281
+ progress_total: int = 1,
282
+ progress_message: str = "Downloading and verifying asset",
283
+ ) -> dict:
186
284
  destination.parent.mkdir(parents=True, exist_ok=True)
187
285
  if destination.is_file():
188
286
  verify_asset(destination, asset["sha256"])
@@ -196,7 +294,18 @@ def materialize(asset: dict, destination: Path, cache_dirs: list[Path]) -> dict:
196
294
  if temporary.exists():
197
295
  temporary.unlink()
198
296
  try:
199
- urllib.request.urlretrieve(asset["url"], temporary)
297
+ def reporthook(block_count: int, block_size: int, total_size: int) -> None:
298
+ if progress is None or not progress_phase or total_size <= 0:
299
+ return
300
+ downloaded = min(block_count * block_size, total_size)
301
+ fraction = (progress_index + downloaded / total_size) / max(progress_total, 1)
302
+ progress.update(
303
+ progress_phase, progress_step, progress_message,
304
+ fraction=fraction, current_file=asset["file"],
305
+ completed_files=progress_index, total_files=progress_total,
306
+ )
307
+
308
+ urllib.request.urlretrieve(asset["url"], temporary, reporthook=reporthook)
200
309
  verify_asset(temporary, asset["sha256"])
201
310
  os.replace(temporary, destination)
202
311
  finally:
@@ -574,6 +683,7 @@ def java_probe() -> dict:
574
683
 
575
684
 
576
685
  def install_command(args: argparse.Namespace) -> dict:
686
+ global ACTIVE_PROGRESS
577
687
  versions = load_versions()
578
688
  install_root = normalized(args.install_root or default_install_root())
579
689
  data_root = normalized(args.data_root or default_data_root())
@@ -595,6 +705,13 @@ def install_command(args: argparse.Namespace) -> dict:
595
705
  }
596
706
  if args.dry_run:
597
707
  return {"success": True, "dryRun": True, "plan": plan}
708
+ progress = ProgressReporter(
709
+ getattr(args, "progress_file", None),
710
+ getattr(args, "operation_id", None),
711
+ getattr(args, "operation_kind", "initialize"),
712
+ )
713
+ ACTIVE_PROGRESS = progress
714
+ progress.update("preflight", 0, "Checking prerequisites")
598
715
  if sys.version_info < (3, 11):
599
716
  raise OnboardingError(f"Python 3.11+ required, got {sys.version.split()[0]}")
600
717
  if not project_root.is_dir():
@@ -605,20 +722,37 @@ def install_command(args: argparse.Namespace) -> dict:
605
722
  verified: list[dict] = []
606
723
 
607
724
  cli_component = components["cli"]
608
- for role in ("wheel", "checksums"):
609
- asset = cli_component[role]
610
- verified.append(materialize(asset, downloads / "cli" / asset["file"], cache_dirs))
725
+ cli_assets = [cli_component[role] for role in ("wheel", "checksums")]
726
+ for index, asset in enumerate(cli_assets):
727
+ progress.update(
728
+ "cli", 1, "Downloading and verifying CLI",
729
+ fraction=index / len(cli_assets), current_file=asset["file"],
730
+ completed_files=index, total_files=len(cli_assets),
731
+ )
732
+ verified.append(materialize(
733
+ asset, downloads / "cli" / asset["file"], cache_dirs,
734
+ progress=progress, progress_phase="cli", progress_step=1,
735
+ progress_index=index, progress_total=len(cli_assets),
736
+ progress_message="Downloading and verifying CLI",
737
+ ))
738
+ progress.update(
739
+ "cli", 1, "Downloading and verifying CLI",
740
+ fraction=(index + 1) / len(cli_assets), current_file=asset["file"],
741
+ completed_files=index + 1, total_files=len(cli_assets),
742
+ )
611
743
  checksum_lines = (downloads / "cli" / cli_component["checksums"]["file"]).read_text(encoding="utf-8").splitlines()
612
744
  checksum_entries = {line.split(maxsplit=1)[1].strip(): line.split(maxsplit=1)[0].lower() for line in checksum_lines if len(line.split(maxsplit=1)) == 2}
613
745
  wheel_asset = cli_component["wheel"]
614
746
  if checksum_entries.get(wheel_asset["file"]) != wheel_asset["sha256"]:
615
747
  raise OnboardingError("Pinned CLI SHA256SUMS does not match the pinned wheel hash")
616
748
  if args.skip_cli_install:
749
+ progress.update("cli", 1, "Using existing CLI", fraction=0.65, current_file="foggy-runtime")
617
750
  cli_command = normalized(args.cli_command or shutil.which("foggy-runtime") or "")
618
751
  if not cli_command.is_file():
619
752
  raise OnboardingError("--skip-cli-install requires --cli-command or foggy-runtime on PATH")
620
753
  cli_mode = "external"
621
754
  else:
755
+ progress.update("cli", 1, "Installing CLI", fraction=0.65, current_file="Python virtual environment")
622
756
  python_path = venv_python(install_root)
623
757
  if not python_path.is_file():
624
758
  venv.EnvBuilder(with_pip=True).create(install_root / "venv")
@@ -633,27 +767,65 @@ def install_command(args: argparse.Namespace) -> dict:
633
767
  )
634
768
  cli_command = venv_cli(install_root)
635
769
  cli_mode = "managed-venv"
770
+ progress.update("cli", 1, "Verifying CLI version", fraction=0.9, current_file="foggy-runtime")
636
771
  cli_version = command_result([str(cli_command), "--version"], check=True)
637
772
  actual_cli_version = version_tuple(cli_version["stdout"])[:3]
638
773
  pinned_cli_version = version_tuple(cli_component["version"])[:3]
639
774
  version_matches = actual_cli_version >= pinned_cli_version if cli_mode == "external" else actual_cli_version == pinned_cli_version
640
775
  if not version_matches:
641
776
  raise OnboardingError(f"Unexpected CLI version: {cli_version['stdout']}")
777
+ progress.update("cli", 1, "CLI ready", fraction=1.0)
642
778
 
643
779
  launcher_dir = install_root / "launcher"
644
- for asset in components["launcher"]["assets"]:
645
- verified.append(materialize(asset, launcher_dir / asset["file"], cache_dirs))
780
+ launcher_assets = components["launcher"]["assets"]
781
+ for index, asset in enumerate(launcher_assets):
782
+ progress.update(
783
+ "launcher", 2, "Downloading and verifying Launcher",
784
+ fraction=index / len(launcher_assets), current_file=asset["file"],
785
+ completed_files=index, total_files=len(launcher_assets),
786
+ )
787
+ verified.append(materialize(
788
+ asset, launcher_dir / asset["file"], cache_dirs,
789
+ progress=progress, progress_phase="launcher", progress_step=2,
790
+ progress_index=index, progress_total=len(launcher_assets),
791
+ progress_message="Downloading and verifying Launcher",
792
+ ))
793
+ progress.update(
794
+ "launcher", 2, "Downloading and verifying Launcher",
795
+ fraction=(index + 1) / len(launcher_assets), current_file=asset["file"],
796
+ completed_files=index + 1, total_files=len(launcher_assets),
797
+ )
646
798
  if os.name != "nt":
647
799
  (launcher_dir / "start-foggy-runtime.sh").chmod(0o755)
800
+ progress.update("launcher", 2, "Launcher ready", fraction=1.0)
648
801
 
649
802
  analysis_assets = components["analysisSkill"]["assets"]
650
- for asset in analysis_assets:
651
- verified.append(materialize(asset, downloads / "skill" / asset["file"], cache_dirs))
803
+ for index, asset in enumerate(analysis_assets):
804
+ progress.update(
805
+ "analysis-skill", 3, "Downloading and verifying analysis Skill",
806
+ fraction=index / len(analysis_assets), current_file=asset["file"],
807
+ completed_files=index, total_files=len(analysis_assets),
808
+ )
809
+ verified.append(materialize(
810
+ asset, downloads / "skill" / asset["file"], cache_dirs,
811
+ progress=progress, progress_phase="analysis-skill", progress_step=3,
812
+ progress_index=index, progress_total=len(analysis_assets),
813
+ progress_message="Downloading and verifying analysis Skill",
814
+ ))
815
+ progress.update(
816
+ "analysis-skill", 3, "Downloading and verifying analysis Skill",
817
+ fraction=(index + 1) / len(analysis_assets), current_file=asset["file"],
818
+ completed_files=index + 1, total_files=len(analysis_assets),
819
+ )
652
820
  zip_asset = next(item for item in analysis_assets if item["role"] == "zip")
821
+ progress.update("analysis-skill", 3, "Installing analysis Skill", fraction=0.9, current_file=zip_asset["file"])
653
822
  analysis_skill = install_analysis_skill(
654
823
  downloads / "skill" / zip_asset["file"], project_root, components["analysisSkill"]["version"], zip_asset["sha256"], args.replace_skill
655
824
  )
825
+ progress.update("analysis-skill", 3, "Analysis Skill ready", fraction=1.0)
826
+ progress.update("workspace-skills", 4, "Installing onboarding Skill", fraction=0.1)
656
827
  onboarding_skill = install_onboarding_skill(project_root)
828
+ progress.update("workspace-skills", 4, "Workspace Skills ready", fraction=1.0)
657
829
  state = {
658
830
  "schemaVersion": STATE_SCHEMA,
659
831
  "installedAt": now_utc(),
@@ -668,7 +840,10 @@ def install_command(args: argparse.Namespace) -> dict:
668
840
  "securityMode": versions["defaults"]["securityMode"],
669
841
  "productionReady": False,
670
842
  }
843
+ progress.update("state", 5, "Writing install state", fraction=0.2, current_file="install-state.json")
671
844
  atomic_json(install_root / "install-state.json", state)
845
+ progress.finish()
846
+ ACTIVE_PROGRESS = None
672
847
  return {
673
848
  "success": True,
674
849
  "schemaVersion": "foggy-deepseek-onboarding-install-result/v1",
@@ -1945,6 +2120,9 @@ def build_parser() -> argparse.ArgumentParser:
1945
2120
  install.add_argument("--replace-skill", action="store_true")
1946
2121
  install.add_argument("--skip-cli-install", action="store_true")
1947
2122
  install.add_argument("--cli-command")
2123
+ install.add_argument("--progress-file")
2124
+ install.add_argument("--operation-id")
2125
+ install.add_argument("--operation-kind", default="initialize")
1948
2126
  install.add_argument("--dry-run", action="store_true")
1949
2127
  install.set_defaults(handler=install_command)
1950
2128
 
@@ -2104,8 +2282,12 @@ def main() -> None:
2104
2282
  exit_code = 1
2105
2283
  emit(result, exit_code)
2106
2284
  except OnboardingError as exc:
2285
+ if ACTIVE_PROGRESS is not None:
2286
+ ACTIVE_PROGRESS.fail(exc)
2107
2287
  emit({"success": False, "error": {"code": "ONBOARDING_ERROR", "message": str(exc)}, "productionReady": False}, 1)
2108
2288
  except Exception as exc:
2289
+ if ACTIVE_PROGRESS is not None:
2290
+ ACTIVE_PROGRESS.fail(exc)
2109
2291
  emit({"success": False, "error": {"code": "UNEXPECTED_ERROR", "message": str(exc)}, "productionReady": False}, 1)
2110
2292
 
2111
2293