scene-capability-engine 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +2513 -0
- package/LICENSE +21 -0
- package/README.md +765 -0
- package/README.zh.md +630 -0
- package/bin/kiro-spec-engine.js +796 -0
- package/bin/kse.js +3 -0
- package/bin/sce.js +3 -0
- package/bin/sco.js +3 -0
- package/docs/331-poc-adaptation-roadmap.md +156 -0
- package/docs/331-poc-dual-track-integration-guide.md +120 -0
- package/docs/331-poc-weekly-delivery-checklist.md +52 -0
- package/docs/OFFLINE_INSTALL.md +96 -0
- package/docs/README.md +279 -0
- package/docs/adopt-migration-guide.md +599 -0
- package/docs/adoption-guide.md +616 -0
- package/docs/agent-hooks-analysis.md +815 -0
- package/docs/architecture.md +733 -0
- package/docs/articles/ai-driven-development-philosophy-and-practice-review.md +208 -0
- package/docs/articles/ai-driven-development-philosophy-and-practice.en.md +459 -0
- package/docs/articles/ai-driven-development-philosophy-and-practice.md +492 -0
- package/docs/autonomous-control-guide.md +851 -0
- package/docs/command-reference.md +1368 -0
- package/docs/community.md +115 -0
- package/docs/cross-tool-guide.md +555 -0
- package/docs/developer-guide.md +619 -0
- package/docs/document-governance.md +865 -0
- package/docs/environment-management-guide.md +526 -0
- package/docs/examples/add-export-command/design.md +194 -0
- package/docs/examples/add-export-command/requirements.md +110 -0
- package/docs/examples/add-export-command/tasks.md +88 -0
- package/docs/examples/add-rest-api/design.md +855 -0
- package/docs/examples/add-rest-api/requirements.md +323 -0
- package/docs/examples/add-rest-api/tasks.md +355 -0
- package/docs/examples/add-user-dashboard/design.md +192 -0
- package/docs/examples/add-user-dashboard/requirements.md +143 -0
- package/docs/examples/add-user-dashboard/tasks.md +91 -0
- package/docs/faq.md +697 -0
- package/docs/handoffs/evidence/ontology/moqui-template-baseline-2026-02-17-232922.json +156 -0
- package/docs/handoffs/evidence/ontology/moqui-template-baseline-2026-02-17-232922.md +24 -0
- package/docs/images/wechat-qr.png +0 -0
- package/docs/integration-modes.md +529 -0
- package/docs/integration-philosophy.md +313 -0
- package/docs/knowledge-management-guide.md +263 -0
- package/docs/manual-workflows-guide.md +418 -0
- package/docs/moqui-capability-matrix.md +73 -0
- package/docs/moqui-template-core-library-playbook.md +109 -0
- package/docs/multi-agent-coordination-guide.md +553 -0
- package/docs/multi-repo-management-guide.md +1344 -0
- package/docs/quick-start-with-ai-tools.md +375 -0
- package/docs/quick-start.md +146 -0
- package/docs/release-checklist.md +121 -0
- package/docs/releases/README.md +13 -0
- package/docs/releases/v1.46.2-validation.md +45 -0
- package/docs/releases/v1.46.2.md +50 -0
- package/docs/scene-runtime-guide.md +347 -0
- package/docs/spec-collaboration-guide.md +369 -0
- package/docs/spec-locking-guide.md +225 -0
- package/docs/spec-numbering-guide.md +348 -0
- package/docs/spec-workflow.md +519 -0
- package/docs/steering-strategy-guide.md +196 -0
- package/docs/team-collaboration-guide.md +465 -0
- package/docs/testing-strategy.md +272 -0
- package/docs/tools/claude-guide.md +654 -0
- package/docs/tools/cursor-guide.md +706 -0
- package/docs/tools/generic-guide.md +446 -0
- package/docs/tools/kiro-guide.md +308 -0
- package/docs/tools/vscode-guide.md +445 -0
- package/docs/tools/windsurf-guide.md +391 -0
- package/docs/troubleshooting.md +1135 -0
- package/docs/upgrade-guide.md +639 -0
- package/docs/value-observability-guide.md +127 -0
- package/docs/zh/README.md +341 -0
- package/docs/zh/quick-start.md +764 -0
- package/docs/zh/release-checklist.md +121 -0
- package/docs/zh/releases/README.md +13 -0
- package/docs/zh/releases/v1.46.2-validation.md +45 -0
- package/docs/zh/releases/v1.46.2.md +50 -0
- package/docs/zh/spec-numbering-guide.md +348 -0
- package/docs/zh/tools/claude-guide.md +349 -0
- package/docs/zh/tools/cursor-guide.md +281 -0
- package/docs/zh/tools/generic-guide.md +499 -0
- package/docs/zh/tools/kiro-guide.md +342 -0
- package/docs/zh/tools/vscode-guide.md +449 -0
- package/docs/zh/tools/windsurf-guide.md +378 -0
- package/docs/zh/value-observability-guide.md +127 -0
- package/docs//344/272/244/344/273/230/346/270/205/345/215/225.md +75 -0
- package/lib/adoption/adoption-logger.js +487 -0
- package/lib/adoption/adoption-strategy.js +538 -0
- package/lib/adoption/backup-manager.js +420 -0
- package/lib/adoption/conflict-resolver.js +410 -0
- package/lib/adoption/detection-engine.js +275 -0
- package/lib/adoption/diff-viewer.js +226 -0
- package/lib/adoption/error-formatter.js +509 -0
- package/lib/adoption/file-classifier.js +385 -0
- package/lib/adoption/progress-reporter.js +534 -0
- package/lib/adoption/smart-orchestrator.js +470 -0
- package/lib/adoption/strategy-selector.js +218 -0
- package/lib/adoption/summary-generator.js +493 -0
- package/lib/adoption/template-sync.js +605 -0
- package/lib/auto/autonomous-engine.js +485 -0
- package/lib/auto/checkpoint-manager.js +300 -0
- package/lib/auto/close-loop-runner.js +2476 -0
- package/lib/auto/config-schema.js +176 -0
- package/lib/auto/decision-engine.js +344 -0
- package/lib/auto/error-recovery-manager.js +580 -0
- package/lib/auto/goal-decomposer.js +278 -0
- package/lib/auto/progress-tracker.js +502 -0
- package/lib/auto/safety-manager.js +186 -0
- package/lib/auto/semantic-decomposer.js +137 -0
- package/lib/auto/state-manager.js +126 -0
- package/lib/auto/task-queue-manager.js +340 -0
- package/lib/backup/backup-system.js +372 -0
- package/lib/backup/selective-backup.js +207 -0
- package/lib/collab/agent-registry.js +240 -0
- package/lib/collab/collab-manager.js +285 -0
- package/lib/collab/contract-manager.js +320 -0
- package/lib/collab/coordinator.js +370 -0
- package/lib/collab/dependency-manager.js +280 -0
- package/lib/collab/index.js +20 -0
- package/lib/collab/integration-manager.js +202 -0
- package/lib/collab/merge-coordinator.js +252 -0
- package/lib/collab/metadata-manager.js +233 -0
- package/lib/collab/multi-agent-config.js +120 -0
- package/lib/collab/spec-lifecycle-manager.js +304 -0
- package/lib/collab/sync-barrier.js +88 -0
- package/lib/collab/visualizer.js +208 -0
- package/lib/commands/adopt.js +749 -0
- package/lib/commands/auto.js +19559 -0
- package/lib/commands/collab.js +275 -0
- package/lib/commands/context.js +99 -0
- package/lib/commands/docs.js +808 -0
- package/lib/commands/doctor.js +273 -0
- package/lib/commands/env.js +420 -0
- package/lib/commands/knowledge.js +309 -0
- package/lib/commands/lock.js +235 -0
- package/lib/commands/ops.js +409 -0
- package/lib/commands/orchestrate.js +446 -0
- package/lib/commands/prompt.js +105 -0
- package/lib/commands/repo.js +118 -0
- package/lib/commands/rollback.js +219 -0
- package/lib/commands/scene.js +15549 -0
- package/lib/commands/spec-bootstrap.js +147 -0
- package/lib/commands/spec-gate.js +157 -0
- package/lib/commands/spec-pipeline.js +205 -0
- package/lib/commands/status.js +321 -0
- package/lib/commands/task.js +199 -0
- package/lib/commands/templates.js +654 -0
- package/lib/commands/upgrade.js +231 -0
- package/lib/commands/value.js +569 -0
- package/lib/commands/watch.js +684 -0
- package/lib/commands/workflows.js +240 -0
- package/lib/commands/workspace-multi.js +325 -0
- package/lib/commands/workspace.js +189 -0
- package/lib/context/context-exporter.js +378 -0
- package/lib/context/prompt-generator.js +482 -0
- package/lib/data/moqui-capability-lexicon.json +45 -0
- package/lib/environment/backup-system.js +189 -0
- package/lib/environment/environment-manager.js +379 -0
- package/lib/environment/environment-registry.js +168 -0
- package/lib/gitignore/gitignore-backup.js +229 -0
- package/lib/gitignore/gitignore-detector.js +239 -0
- package/lib/gitignore/gitignore-integration.js +267 -0
- package/lib/gitignore/gitignore-transformer.js +193 -0
- package/lib/gitignore/layered-rules-template.js +42 -0
- package/lib/governance/archive-tool.js +284 -0
- package/lib/governance/cleanup-tool.js +237 -0
- package/lib/governance/config-manager.js +186 -0
- package/lib/governance/diagnostic-engine.js +271 -0
- package/lib/governance/doc-reference-checker.js +200 -0
- package/lib/governance/execution-logger.js +243 -0
- package/lib/governance/file-scanner.js +285 -0
- package/lib/governance/hooks-manager.js +333 -0
- package/lib/governance/reporter.js +337 -0
- package/lib/governance/validation-engine.js +181 -0
- package/lib/i18n.js +79 -0
- package/lib/knowledge/entry-manager.js +208 -0
- package/lib/knowledge/index-manager.js +261 -0
- package/lib/knowledge/knowledge-manager.js +273 -0
- package/lib/knowledge/template-manager.js +191 -0
- package/lib/lock/index.js +21 -0
- package/lib/lock/lock-file.js +192 -0
- package/lib/lock/lock-manager.js +321 -0
- package/lib/lock/machine-identifier.js +135 -0
- package/lib/lock/steering-file-lock.js +207 -0
- package/lib/lock/task-lock-manager.js +345 -0
- package/lib/operations/audit-logger.js +293 -0
- package/lib/operations/feedback-manager.js +1147 -0
- package/lib/operations/index.js +23 -0
- package/lib/operations/models/index.js +170 -0
- package/lib/operations/operations-manager.js +151 -0
- package/lib/operations/operations-validator.js +280 -0
- package/lib/operations/permission-manager.js +354 -0
- package/lib/operations/template-loader.js +143 -0
- package/lib/orchestrator/agent-spawner.js +629 -0
- package/lib/orchestrator/bootstrap-prompt-builder.js +236 -0
- package/lib/orchestrator/index.js +19 -0
- package/lib/orchestrator/orchestration-engine.js +1270 -0
- package/lib/orchestrator/orchestrator-config.js +173 -0
- package/lib/orchestrator/status-monitor.js +591 -0
- package/lib/python-checker.js +209 -0
- package/lib/repo/config-manager.js +580 -0
- package/lib/repo/errors/config-error.js +13 -0
- package/lib/repo/errors/git-error.js +15 -0
- package/lib/repo/errors/repo-error.js +14 -0
- package/lib/repo/git-operations.js +181 -0
- package/lib/repo/handlers/.gitkeep +1 -0
- package/lib/repo/handlers/exec-handler.js +155 -0
- package/lib/repo/handlers/health-handler.js +169 -0
- package/lib/repo/handlers/init-handler.js +197 -0
- package/lib/repo/handlers/status-handler.js +176 -0
- package/lib/repo/output-formatter.js +184 -0
- package/lib/repo/path-resolver.js +178 -0
- package/lib/repo/repo-manager.js +514 -0
- package/lib/scene-runtime/audit-emitter.js +59 -0
- package/lib/scene-runtime/binding-plugin-loader.js +351 -0
- package/lib/scene-runtime/binding-registry.js +349 -0
- package/lib/scene-runtime/eval-bridge.js +44 -0
- package/lib/scene-runtime/index.js +19 -0
- package/lib/scene-runtime/moqui-adapter.js +620 -0
- package/lib/scene-runtime/moqui-client.js +606 -0
- package/lib/scene-runtime/moqui-extractor.js +2029 -0
- package/lib/scene-runtime/plan-compiler.js +208 -0
- package/lib/scene-runtime/policy-gate.js +58 -0
- package/lib/scene-runtime/runtime-executor.js +358 -0
- package/lib/scene-runtime/scene-loader.js +96 -0
- package/lib/scene-runtime/scene-ontology.js +959 -0
- package/lib/scene-runtime/scene-template-linter.js +852 -0
- package/lib/scene-runtime/templates/scene-template-erp-query-v0.1.yaml +28 -0
- package/lib/scene-runtime/templates/scene-template-hybrid-shadow-v0.1.yaml +34 -0
- package/lib/spec/bootstrap/context-collector.js +48 -0
- package/lib/spec/bootstrap/draft-generator.js +158 -0
- package/lib/spec/bootstrap/questionnaire-engine.js +70 -0
- package/lib/spec/bootstrap/trace-emitter.js +59 -0
- package/lib/spec/multi-spec-orchestrate.js +93 -0
- package/lib/spec/pipeline/constants.js +6 -0
- package/lib/spec/pipeline/stage-adapters.js +118 -0
- package/lib/spec/pipeline/stage-runner.js +146 -0
- package/lib/spec/pipeline/state-store.js +119 -0
- package/lib/spec-gate/engine/gate-engine.js +165 -0
- package/lib/spec-gate/policy/default-policy.js +22 -0
- package/lib/spec-gate/policy/policy-loader.js +103 -0
- package/lib/spec-gate/result-emitter.js +81 -0
- package/lib/spec-gate/rules/default-rules.js +156 -0
- package/lib/spec-gate/rules/rule-registry.js +51 -0
- package/lib/steering/adoption-config.js +164 -0
- package/lib/steering/compliance-auto-fixer.js +204 -0
- package/lib/steering/compliance-cache.js +99 -0
- package/lib/steering/compliance-error-reporter.js +70 -0
- package/lib/steering/context-sync-manager.js +273 -0
- package/lib/steering/index.js +92 -0
- package/lib/steering/spec-steering.js +230 -0
- package/lib/steering/steering-compliance-checker.js +73 -0
- package/lib/steering/steering-loader.js +144 -0
- package/lib/steering/steering-manager.js +289 -0
- package/lib/task/index.js +12 -0
- package/lib/task/task-claimer.js +489 -0
- package/lib/task/task-status-store.js +418 -0
- package/lib/templates/cache-manager.js +440 -0
- package/lib/templates/content-generalizer.js +247 -0
- package/lib/templates/frontmatter-generator.js +128 -0
- package/lib/templates/git-handler.js +471 -0
- package/lib/templates/metadata-collector.js +328 -0
- package/lib/templates/path-utils.js +144 -0
- package/lib/templates/registry-parser.js +505 -0
- package/lib/templates/spec-reader.js +216 -0
- package/lib/templates/template-applicator.js +249 -0
- package/lib/templates/template-creator.js +256 -0
- package/lib/templates/template-error.js +143 -0
- package/lib/templates/template-exporter.js +502 -0
- package/lib/templates/template-manager.js +782 -0
- package/lib/templates/template-validator.js +361 -0
- package/lib/upgrade/migration-engine.js +382 -0
- package/lib/upgrade/migrations/.gitkeep +52 -0
- package/lib/upgrade/migrations/1.0.0-to-1.1.0.js +78 -0
- package/lib/utils/file-diff.js +177 -0
- package/lib/utils/fs-utils.js +274 -0
- package/lib/utils/tool-detector.js +383 -0
- package/lib/utils/validation.js +324 -0
- package/lib/value/gate-summary-emitter.js +99 -0
- package/lib/value/metric-contract-loader.js +210 -0
- package/lib/value/risk-evaluator.js +117 -0
- package/lib/value/weekly-snapshot-builder.js +61 -0
- package/lib/version/version-checker.js +156 -0
- package/lib/version/version-manager.js +327 -0
- package/lib/watch/action-executor.js +458 -0
- package/lib/watch/event-debouncer.js +323 -0
- package/lib/watch/execution-logger.js +550 -0
- package/lib/watch/file-watcher.js +499 -0
- package/lib/watch/presets.js +266 -0
- package/lib/watch/watch-manager.js +533 -0
- package/lib/workspace/multi/global-config.js +150 -0
- package/lib/workspace/multi/index.js +22 -0
- package/lib/workspace/multi/path-utils.js +173 -0
- package/lib/workspace/multi/workspace-context-resolver.js +244 -0
- package/lib/workspace/multi/workspace-registry.js +196 -0
- package/lib/workspace/multi/workspace-state-manager.js +537 -0
- package/lib/workspace/multi/workspace.js +90 -0
- package/lib/workspace/workspace-manager.js +370 -0
- package/lib/workspace/workspace-sync.js +356 -0
- package/locales/en.json +114 -0
- package/locales/zh.json +114 -0
- package/package.json +102 -0
- package/template/.kiro/README.md +247 -0
- package/template/.kiro/hooks/check-spec-on-create.kiro.hook +17 -0
- package/template/.kiro/hooks/run-tests-on-save.kiro.hook +13 -0
- package/template/.kiro/hooks/sync-tasks-on-edit.kiro.hook +16 -0
- package/template/.kiro/specs/SPEC_WORKFLOW_GUIDE.md +134 -0
- package/template/.kiro/steering/CORE_PRINCIPLES.md +133 -0
- package/template/.kiro/steering/CURRENT_CONTEXT.md +30 -0
- package/template/.kiro/steering/ENVIRONMENT.md +35 -0
- package/template/.kiro/steering/RULES_GUIDE.md +46 -0
- package/template/.kiro/templates/operations/default/change-impact.md +112 -0
- package/template/.kiro/templates/operations/default/deployment.md +91 -0
- package/template/.kiro/templates/operations/default/feedback-response.md +269 -0
- package/template/.kiro/templates/operations/default/migration-plan.md +172 -0
- package/template/.kiro/templates/operations/default/monitoring.md +135 -0
- package/template/.kiro/templates/operations/default/operations.md +135 -0
- package/template/.kiro/templates/operations/default/rollback.md +143 -0
- package/template/.kiro/templates/operations/default/tools.yaml +364 -0
- package/template/.kiro/templates/operations/default/troubleshooting.md +123 -0
- package/template/.kiro/tools/backup_manager.py +295 -0
- package/template/.kiro/tools/configuration_manager.py +218 -0
- package/template/.kiro/tools/document_evaluator.py +550 -0
- package/template/.kiro/tools/enhancement_logger.py +168 -0
- package/template/.kiro/tools/error_handler.py +335 -0
- package/template/.kiro/tools/improvement_identifier.py +444 -0
- package/template/.kiro/tools/modification_applicator.py +737 -0
- package/template/.kiro/tools/quality_gate_enforcer.py +207 -0
- package/template/.kiro/tools/quality_scorer.py +305 -0
- package/template/.kiro/tools/report_generator.py +154 -0
- package/template/.kiro/tools/ultrawork_enhancer.py +676 -0
- package/template/.kiro/tools/ultrawork_enhancer_refactored.py +0 -0
- package/template/.kiro/tools/ultrawork_enhancer_v2.py +463 -0
- package/template/.kiro/tools/ultrawork_enhancer_v3.py +606 -0
- package/template/.kiro/tools/workflow_quality_gate.py +100 -0
- package/template/README.md +111 -0
|
@@ -0,0 +1,537 @@
|
|
|
1
|
+
const fs = require('fs-extra');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const os = require('os');
|
|
4
|
+
const Workspace = require('./workspace');
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* WorkspaceStateManager - Single Source of Truth for workspace state
|
|
8
|
+
*
|
|
9
|
+
* Implements the Data Atomicity Principle by managing all workspace-related
|
|
10
|
+
* data in a single configuration file. This ensures atomic updates and
|
|
11
|
+
* eliminates data inconsistency risks.
|
|
12
|
+
*
|
|
13
|
+
* Architecture: Single file (~/.kse/workspace-state.json) contains:
|
|
14
|
+
* - All workspace entries
|
|
15
|
+
* - Active workspace selection
|
|
16
|
+
* - User preferences
|
|
17
|
+
*
|
|
18
|
+
* This replaces the previous dual-file approach (workspaces.json + config.json)
|
|
19
|
+
* which violated the Single Source of Truth principle.
|
|
20
|
+
*/
|
|
21
|
+
class WorkspaceStateManager {
|
|
22
|
+
/**
|
|
23
|
+
* Create a new WorkspaceStateManager instance
|
|
24
|
+
*
|
|
25
|
+
* @param {string} statePath - Path to workspace-state.json (optional)
|
|
26
|
+
*/
|
|
27
|
+
constructor(statePath = null) {
|
|
28
|
+
this.statePath = statePath || this.getDefaultStatePath();
|
|
29
|
+
this.state = {
|
|
30
|
+
version: '1.0',
|
|
31
|
+
activeWorkspace: null,
|
|
32
|
+
workspaces: new Map(),
|
|
33
|
+
preferences: {
|
|
34
|
+
autoDetectWorkspace: true,
|
|
35
|
+
confirmDestructiveOperations: true
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
this.loaded = false;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Get the default state file path
|
|
43
|
+
*
|
|
44
|
+
* @returns {string} Path to ~/.kse/workspace-state.json
|
|
45
|
+
*/
|
|
46
|
+
getDefaultStatePath() {
|
|
47
|
+
const homeDir = os.homedir();
|
|
48
|
+
return path.join(homeDir, '.kse', 'workspace-state.json');
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Load workspace state from disk
|
|
53
|
+
*
|
|
54
|
+
* Supports automatic migration from legacy format (workspaces.json + config.json)
|
|
55
|
+
*
|
|
56
|
+
* @returns {Promise<boolean>} True if loaded successfully
|
|
57
|
+
*/
|
|
58
|
+
async load() {
|
|
59
|
+
try {
|
|
60
|
+
// Try loading new format
|
|
61
|
+
const exists = await fs.pathExists(this.statePath);
|
|
62
|
+
|
|
63
|
+
if (exists) {
|
|
64
|
+
await this.loadNewFormat();
|
|
65
|
+
this.loaded = true;
|
|
66
|
+
return true;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Check for legacy format and migrate
|
|
70
|
+
if (await this.hasLegacyFiles()) {
|
|
71
|
+
console.log('Migrating workspace configuration to new format...');
|
|
72
|
+
await this.migrateFromLegacy();
|
|
73
|
+
this.loaded = true;
|
|
74
|
+
return true;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Initialize empty state
|
|
78
|
+
this.state = {
|
|
79
|
+
version: '1.0',
|
|
80
|
+
activeWorkspace: null,
|
|
81
|
+
workspaces: new Map(),
|
|
82
|
+
preferences: {
|
|
83
|
+
autoDetectWorkspace: true,
|
|
84
|
+
confirmDestructiveOperations: true
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
this.loaded = true;
|
|
88
|
+
return true;
|
|
89
|
+
|
|
90
|
+
} catch (error) {
|
|
91
|
+
if (error instanceof SyntaxError) {
|
|
92
|
+
throw new Error(`Workspace state file is corrupted: ${this.statePath}. ` +
|
|
93
|
+
`Please backup and delete the file, then try again.`);
|
|
94
|
+
}
|
|
95
|
+
throw error;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Load state from new format file
|
|
101
|
+
*
|
|
102
|
+
* @private
|
|
103
|
+
*/
|
|
104
|
+
async loadNewFormat() {
|
|
105
|
+
const content = await fs.readFile(this.statePath, 'utf8');
|
|
106
|
+
const data = JSON.parse(content);
|
|
107
|
+
|
|
108
|
+
// Validate version
|
|
109
|
+
if (data.version !== this.state.version) {
|
|
110
|
+
console.warn(`Warning: State version mismatch. Expected ${this.state.version}, got ${data.version}`);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Load state
|
|
114
|
+
this.state.version = data.version;
|
|
115
|
+
this.state.activeWorkspace = data.activeWorkspace || null;
|
|
116
|
+
|
|
117
|
+
// Load workspaces
|
|
118
|
+
this.state.workspaces = new Map();
|
|
119
|
+
if (data.workspaces && Array.isArray(data.workspaces)) {
|
|
120
|
+
for (const workspaceData of data.workspaces) {
|
|
121
|
+
const workspace = Workspace.fromDict(workspaceData);
|
|
122
|
+
this.state.workspaces.set(workspace.name, workspace);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Load preferences
|
|
127
|
+
this.state.preferences = {
|
|
128
|
+
autoDetectWorkspace: data.preferences?.autoDetectWorkspace ?? true,
|
|
129
|
+
confirmDestructiveOperations: data.preferences?.confirmDestructiveOperations ?? true
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Check if legacy configuration files exist
|
|
135
|
+
*
|
|
136
|
+
* @private
|
|
137
|
+
* @returns {Promise<boolean>}
|
|
138
|
+
*/
|
|
139
|
+
async hasLegacyFiles() {
|
|
140
|
+
const homeDir = os.homedir();
|
|
141
|
+
const legacyWorkspacesPath = path.join(homeDir, '.kse', 'workspaces.json');
|
|
142
|
+
const legacyConfigPath = path.join(homeDir, '.kse', 'config.json');
|
|
143
|
+
|
|
144
|
+
const workspacesExists = await fs.pathExists(legacyWorkspacesPath);
|
|
145
|
+
const configExists = await fs.pathExists(legacyConfigPath);
|
|
146
|
+
|
|
147
|
+
return workspacesExists || configExists;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Migrate from legacy format (workspaces.json + config.json)
|
|
152
|
+
*
|
|
153
|
+
* @private
|
|
154
|
+
*/
|
|
155
|
+
async migrateFromLegacy() {
|
|
156
|
+
const homeDir = os.homedir();
|
|
157
|
+
const legacyWorkspacesPath = path.join(homeDir, '.kse', 'workspaces.json');
|
|
158
|
+
const legacyConfigPath = path.join(homeDir, '.kse', 'config.json');
|
|
159
|
+
|
|
160
|
+
// Load legacy workspaces
|
|
161
|
+
let legacyWorkspaces = [];
|
|
162
|
+
if (await fs.pathExists(legacyWorkspacesPath)) {
|
|
163
|
+
const content = await fs.readFile(legacyWorkspacesPath, 'utf8');
|
|
164
|
+
const data = JSON.parse(content);
|
|
165
|
+
legacyWorkspaces = data.workspaces || [];
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Load legacy config
|
|
169
|
+
let legacyActiveWorkspace = null;
|
|
170
|
+
let legacyPreferences = {};
|
|
171
|
+
if (await fs.pathExists(legacyConfigPath)) {
|
|
172
|
+
const content = await fs.readFile(legacyConfigPath, 'utf8');
|
|
173
|
+
const data = JSON.parse(content);
|
|
174
|
+
legacyActiveWorkspace = data.active_workspace || null;
|
|
175
|
+
legacyPreferences = data.preferences || {};
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Merge into new format
|
|
179
|
+
this.state.activeWorkspace = legacyActiveWorkspace;
|
|
180
|
+
this.state.workspaces = new Map();
|
|
181
|
+
|
|
182
|
+
for (const workspaceData of legacyWorkspaces) {
|
|
183
|
+
const workspace = Workspace.fromDict(workspaceData);
|
|
184
|
+
this.state.workspaces.set(workspace.name, workspace);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
this.state.preferences = {
|
|
188
|
+
autoDetectWorkspace: legacyPreferences.auto_detect_workspace ?? true,
|
|
189
|
+
confirmDestructiveOperations: legacyPreferences.confirm_destructive_operations ?? true
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
// Save to new format
|
|
193
|
+
await this.save();
|
|
194
|
+
|
|
195
|
+
// Backup legacy files
|
|
196
|
+
await this.backupLegacyFiles();
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Backup legacy configuration files
|
|
201
|
+
*
|
|
202
|
+
* @private
|
|
203
|
+
*/
|
|
204
|
+
async backupLegacyFiles() {
|
|
205
|
+
const homeDir = os.homedir();
|
|
206
|
+
const legacyWorkspacesPath = path.join(homeDir, '.kse', 'workspaces.json');
|
|
207
|
+
const legacyConfigPath = path.join(homeDir, '.kse', 'config.json');
|
|
208
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
209
|
+
|
|
210
|
+
if (await fs.pathExists(legacyWorkspacesPath)) {
|
|
211
|
+
const backupPath = path.join(homeDir, '.kse', `workspaces.json.backup-${timestamp}`);
|
|
212
|
+
await fs.copy(legacyWorkspacesPath, backupPath);
|
|
213
|
+
await fs.remove(legacyWorkspacesPath);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
if (await fs.pathExists(legacyConfigPath)) {
|
|
217
|
+
const backupPath = path.join(homeDir, '.kse', `config.json.backup-${timestamp}`);
|
|
218
|
+
await fs.copy(legacyConfigPath, backupPath);
|
|
219
|
+
await fs.remove(legacyConfigPath);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Save workspace state to disk (atomic operation)
|
|
225
|
+
*
|
|
226
|
+
* Uses temp file + atomic rename to ensure consistency
|
|
227
|
+
*
|
|
228
|
+
* @returns {Promise<boolean>} True if saved successfully
|
|
229
|
+
*/
|
|
230
|
+
async save() {
|
|
231
|
+
try {
|
|
232
|
+
// Ensure directory exists
|
|
233
|
+
const stateDir = path.dirname(this.statePath);
|
|
234
|
+
await fs.ensureDir(stateDir);
|
|
235
|
+
|
|
236
|
+
// Serialize state
|
|
237
|
+
const workspacesArray = Array.from(this.state.workspaces.values()).map(ws => ws.toDict());
|
|
238
|
+
|
|
239
|
+
const data = {
|
|
240
|
+
version: this.state.version,
|
|
241
|
+
activeWorkspace: this.state.activeWorkspace,
|
|
242
|
+
workspaces: workspacesArray,
|
|
243
|
+
preferences: {
|
|
244
|
+
autoDetectWorkspace: this.state.preferences.autoDetectWorkspace,
|
|
245
|
+
confirmDestructiveOperations: this.state.preferences.confirmDestructiveOperations
|
|
246
|
+
}
|
|
247
|
+
};
|
|
248
|
+
|
|
249
|
+
// Write to temp file first
|
|
250
|
+
const tempPath = `${this.statePath}.tmp`;
|
|
251
|
+
await fs.writeFile(tempPath, JSON.stringify(data, null, 2), 'utf8');
|
|
252
|
+
|
|
253
|
+
// Atomic rename (ensures consistency)
|
|
254
|
+
await fs.rename(tempPath, this.statePath);
|
|
255
|
+
|
|
256
|
+
return true;
|
|
257
|
+
} catch (error) {
|
|
258
|
+
throw new Error(`Failed to save workspace state: ${error.message}`);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Ensure state is loaded before operations
|
|
264
|
+
*
|
|
265
|
+
* @private
|
|
266
|
+
*/
|
|
267
|
+
async ensureLoaded() {
|
|
268
|
+
if (!this.loaded) {
|
|
269
|
+
await this.load();
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// ==================== Workspace Operations ====================
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* Create a new workspace (atomic operation)
|
|
277
|
+
*
|
|
278
|
+
* @param {string} name - Unique workspace name
|
|
279
|
+
* @param {string} workspacePath - Path to workspace directory
|
|
280
|
+
* @returns {Promise<Workspace>} Created workspace
|
|
281
|
+
* @throws {Error} If name already exists or path is invalid
|
|
282
|
+
*/
|
|
283
|
+
async createWorkspace(name, workspacePath) {
|
|
284
|
+
await this.ensureLoaded();
|
|
285
|
+
|
|
286
|
+
// Validate name
|
|
287
|
+
if (!name || typeof name !== 'string' || name.trim().length === 0) {
|
|
288
|
+
throw new Error('Workspace name cannot be empty');
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// Check for duplicate name
|
|
292
|
+
if (this.state.workspaces.has(name)) {
|
|
293
|
+
throw new Error(`Workspace "${name}" already exists`);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// Validate path (check for .kiro directory)
|
|
297
|
+
const kiroPath = path.join(workspacePath, '.kiro');
|
|
298
|
+
const kiroExists = await fs.pathExists(kiroPath);
|
|
299
|
+
if (!kiroExists) {
|
|
300
|
+
throw new Error(`Path "${workspacePath}" is not a valid kse project directory. ` +
|
|
301
|
+
`Ensure it exists and contains a .kiro/ directory.`);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// Create workspace
|
|
305
|
+
const workspace = new Workspace(name, workspacePath);
|
|
306
|
+
this.state.workspaces.set(name, workspace);
|
|
307
|
+
|
|
308
|
+
// Atomic save
|
|
309
|
+
await this.save();
|
|
310
|
+
|
|
311
|
+
return workspace;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/**
|
|
315
|
+
* Get a workspace by name
|
|
316
|
+
*
|
|
317
|
+
* @param {string} name - Workspace name
|
|
318
|
+
* @returns {Promise<Workspace|null>} Workspace or null if not found
|
|
319
|
+
*/
|
|
320
|
+
async getWorkspace(name) {
|
|
321
|
+
await this.ensureLoaded();
|
|
322
|
+
return this.state.workspaces.get(name) || null;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* List all registered workspaces
|
|
327
|
+
*
|
|
328
|
+
* @returns {Promise<Array<Workspace>>} Array of workspaces
|
|
329
|
+
*/
|
|
330
|
+
async listWorkspaces() {
|
|
331
|
+
await this.ensureLoaded();
|
|
332
|
+
return Array.from(this.state.workspaces.values());
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/**
|
|
336
|
+
* Remove a workspace from the registry (atomic operation)
|
|
337
|
+
*
|
|
338
|
+
* @param {string} name - Workspace name
|
|
339
|
+
* @returns {Promise<boolean>} True if removed, false if not found
|
|
340
|
+
*/
|
|
341
|
+
async removeWorkspace(name) {
|
|
342
|
+
await this.ensureLoaded();
|
|
343
|
+
|
|
344
|
+
if (!this.state.workspaces.has(name)) {
|
|
345
|
+
return false;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
// Remove workspace
|
|
349
|
+
this.state.workspaces.delete(name);
|
|
350
|
+
|
|
351
|
+
// Clear active workspace if it was the removed one
|
|
352
|
+
if (this.state.activeWorkspace === name) {
|
|
353
|
+
this.state.activeWorkspace = null;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
// Atomic save
|
|
357
|
+
await this.save();
|
|
358
|
+
|
|
359
|
+
return true;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
/**
|
|
363
|
+
* Switch to a workspace (atomic operation)
|
|
364
|
+
*
|
|
365
|
+
* Updates both active workspace and last accessed timestamp atomically
|
|
366
|
+
*
|
|
367
|
+
* @param {string} name - Workspace name
|
|
368
|
+
* @returns {Promise<void>}
|
|
369
|
+
* @throws {Error} If workspace doesn't exist
|
|
370
|
+
*/
|
|
371
|
+
async switchWorkspace(name) {
|
|
372
|
+
await this.ensureLoaded();
|
|
373
|
+
|
|
374
|
+
const workspace = this.state.workspaces.get(name);
|
|
375
|
+
if (!workspace) {
|
|
376
|
+
const available = Array.from(this.state.workspaces.keys());
|
|
377
|
+
throw new Error(
|
|
378
|
+
`Workspace "${name}" does not exist.\n` +
|
|
379
|
+
`Available workspaces: ${available.join(', ') || 'none'}`
|
|
380
|
+
);
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
// Update state atomically
|
|
384
|
+
this.state.activeWorkspace = name;
|
|
385
|
+
workspace.updateLastAccessed();
|
|
386
|
+
|
|
387
|
+
// Atomic save
|
|
388
|
+
await this.save();
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
/**
|
|
392
|
+
* Get the active workspace
|
|
393
|
+
*
|
|
394
|
+
* @returns {Promise<Workspace|null>} Active workspace or null
|
|
395
|
+
*/
|
|
396
|
+
async getActiveWorkspace() {
|
|
397
|
+
await this.ensureLoaded();
|
|
398
|
+
|
|
399
|
+
if (!this.state.activeWorkspace) {
|
|
400
|
+
return null;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
const workspace = this.state.workspaces.get(this.state.activeWorkspace);
|
|
404
|
+
if (!workspace) {
|
|
405
|
+
// Active workspace no longer exists, clear it
|
|
406
|
+
this.state.activeWorkspace = null;
|
|
407
|
+
await this.save();
|
|
408
|
+
return null;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
return workspace;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
/**
|
|
415
|
+
* Clear the active workspace
|
|
416
|
+
*
|
|
417
|
+
* @returns {Promise<void>}
|
|
418
|
+
*/
|
|
419
|
+
async clearActiveWorkspace() {
|
|
420
|
+
await this.ensureLoaded();
|
|
421
|
+
this.state.activeWorkspace = null;
|
|
422
|
+
await this.save();
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
/**
|
|
426
|
+
* Find workspace that contains the given path
|
|
427
|
+
*
|
|
428
|
+
* @param {string} targetPath - Path to search for
|
|
429
|
+
* @returns {Promise<Workspace|null>} Workspace containing the path, or null
|
|
430
|
+
*/
|
|
431
|
+
async findWorkspaceByPath(targetPath) {
|
|
432
|
+
await this.ensureLoaded();
|
|
433
|
+
|
|
434
|
+
const absolutePath = path.isAbsolute(targetPath)
|
|
435
|
+
? targetPath
|
|
436
|
+
: path.resolve(targetPath);
|
|
437
|
+
|
|
438
|
+
for (const workspace of this.state.workspaces.values()) {
|
|
439
|
+
if (workspace.containsPath(absolutePath)) {
|
|
440
|
+
return workspace;
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
return null;
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
// ==================== Preference Operations ====================
|
|
448
|
+
|
|
449
|
+
/**
|
|
450
|
+
* Get a preference value
|
|
451
|
+
*
|
|
452
|
+
* @param {string} key - Preference key
|
|
453
|
+
* @returns {Promise<any>} Preference value
|
|
454
|
+
*/
|
|
455
|
+
async getPreference(key) {
|
|
456
|
+
await this.ensureLoaded();
|
|
457
|
+
return this.state.preferences[key];
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
/**
|
|
461
|
+
* Set a preference value
|
|
462
|
+
*
|
|
463
|
+
* @param {string} key - Preference key
|
|
464
|
+
* @param {any} value - Preference value
|
|
465
|
+
* @returns {Promise<void>}
|
|
466
|
+
*/
|
|
467
|
+
async setPreference(key, value) {
|
|
468
|
+
await this.ensureLoaded();
|
|
469
|
+
this.state.preferences[key] = value;
|
|
470
|
+
await this.save();
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
/**
|
|
474
|
+
* Get all preferences
|
|
475
|
+
*
|
|
476
|
+
* @returns {Promise<Object>} All preferences
|
|
477
|
+
*/
|
|
478
|
+
async getPreferences() {
|
|
479
|
+
await this.ensureLoaded();
|
|
480
|
+
return { ...this.state.preferences };
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
// ==================== Utility Methods ====================
|
|
484
|
+
|
|
485
|
+
/**
|
|
486
|
+
* Check if a workspace name exists
|
|
487
|
+
*
|
|
488
|
+
* @param {string} name - Workspace name
|
|
489
|
+
* @returns {Promise<boolean>} True if exists
|
|
490
|
+
*/
|
|
491
|
+
async hasWorkspace(name) {
|
|
492
|
+
await this.ensureLoaded();
|
|
493
|
+
return this.state.workspaces.has(name);
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
/**
|
|
497
|
+
* Get count of registered workspaces
|
|
498
|
+
*
|
|
499
|
+
* @returns {Promise<number>} Number of workspaces
|
|
500
|
+
*/
|
|
501
|
+
async count() {
|
|
502
|
+
await this.ensureLoaded();
|
|
503
|
+
return this.state.workspaces.size;
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
/**
|
|
507
|
+
* Clear all workspaces (for testing purposes)
|
|
508
|
+
*
|
|
509
|
+
* @returns {Promise<void>}
|
|
510
|
+
*/
|
|
511
|
+
async clear() {
|
|
512
|
+
await this.ensureLoaded();
|
|
513
|
+
this.state.workspaces.clear();
|
|
514
|
+
this.state.activeWorkspace = null;
|
|
515
|
+
await this.save();
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
/**
|
|
519
|
+
* Reset to default state
|
|
520
|
+
*
|
|
521
|
+
* @returns {Promise<void>}
|
|
522
|
+
*/
|
|
523
|
+
async reset() {
|
|
524
|
+
this.state = {
|
|
525
|
+
version: '1.0',
|
|
526
|
+
activeWorkspace: null,
|
|
527
|
+
workspaces: new Map(),
|
|
528
|
+
preferences: {
|
|
529
|
+
autoDetectWorkspace: true,
|
|
530
|
+
confirmDestructiveOperations: true
|
|
531
|
+
}
|
|
532
|
+
};
|
|
533
|
+
await this.save();
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
module.exports = WorkspaceStateManager;
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
const PathUtils = require('./path-utils');
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Workspace - Data model for a registered kse project workspace
|
|
5
|
+
*
|
|
6
|
+
* Represents a single workspace entry in the workspace registry.
|
|
7
|
+
* Each workspace has a unique name, absolute path, and timestamps.
|
|
8
|
+
*/
|
|
9
|
+
class Workspace {
|
|
10
|
+
/**
|
|
11
|
+
* Create a new Workspace instance
|
|
12
|
+
*
|
|
13
|
+
* @param {string} name - Unique workspace name
|
|
14
|
+
* @param {string} workspacePath - Absolute path to the workspace directory
|
|
15
|
+
* @param {Date|string} createdAt - Creation timestamp (optional, defaults to now)
|
|
16
|
+
* @param {Date|string} lastAccessed - Last accessed timestamp (optional, defaults to now)
|
|
17
|
+
*/
|
|
18
|
+
constructor(name, workspacePath, createdAt = null, lastAccessed = null) {
|
|
19
|
+
this.name = name;
|
|
20
|
+
this.path = PathUtils.normalize(workspacePath);
|
|
21
|
+
this.createdAt = createdAt ? new Date(createdAt) : new Date();
|
|
22
|
+
this.lastAccessed = lastAccessed ? new Date(lastAccessed) : new Date();
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Get platform-specific path for runtime use
|
|
27
|
+
*
|
|
28
|
+
* @returns {string} Platform-specific path
|
|
29
|
+
*/
|
|
30
|
+
getPlatformPath() {
|
|
31
|
+
return PathUtils.toPlatform(this.path);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Serialize workspace to JSON-compatible object
|
|
36
|
+
*
|
|
37
|
+
* @returns {Object} Serialized workspace data
|
|
38
|
+
*/
|
|
39
|
+
toDict() {
|
|
40
|
+
return {
|
|
41
|
+
name: this.name,
|
|
42
|
+
path: this.path,
|
|
43
|
+
createdAt: this.createdAt.toISOString(),
|
|
44
|
+
lastAccessed: this.lastAccessed.toISOString()
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Deserialize workspace from JSON-compatible object
|
|
50
|
+
*
|
|
51
|
+
* @param {Object} data - Serialized workspace data
|
|
52
|
+
* @returns {Workspace} Workspace instance
|
|
53
|
+
*/
|
|
54
|
+
static fromDict(data) {
|
|
55
|
+
return new Workspace(
|
|
56
|
+
data.name,
|
|
57
|
+
data.path,
|
|
58
|
+
data.createdAt,
|
|
59
|
+
data.lastAccessed
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Update last accessed timestamp to current time
|
|
65
|
+
*/
|
|
66
|
+
updateLastAccessed() {
|
|
67
|
+
this.lastAccessed = new Date();
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Check if this workspace path matches or contains the given path
|
|
72
|
+
*
|
|
73
|
+
* @param {string} targetPath - Path to check
|
|
74
|
+
* @returns {boolean} True if targetPath is within this workspace
|
|
75
|
+
*/
|
|
76
|
+
containsPath(targetPath) {
|
|
77
|
+
return PathUtils.isWithin(targetPath, this.path);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Get a string representation of the workspace
|
|
82
|
+
*
|
|
83
|
+
* @returns {string} String representation
|
|
84
|
+
*/
|
|
85
|
+
toString() {
|
|
86
|
+
return `Workspace(name="${this.name}", path="${this.path}")`;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
module.exports = Workspace;
|