@openspecui/core 4.1.0 → 6.0.0-beta.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/dist/cli-projection-DxnVGm2B.d.mts +789 -0
- package/dist/cli-projection-vO3-FJg5.mjs +161 -0
- package/dist/cli-projection.d.mts +6 -0
- package/dist/cli-projection.mjs +4 -0
- package/dist/command-result-fkH3Ekdz.d.mts +8477 -0
- package/dist/common-D8YUK9dA.mjs +57 -0
- package/dist/dashboard-summary-transport-CQ4is5_e.d.mts +66227 -0
- package/dist/dashboard-summary-transport-D2VBAIxg.mjs +175 -0
- package/dist/dashboard-summary-transport.d.mts +25 -0
- package/dist/dashboard-summary-transport.mjs +6 -0
- package/dist/{document-translation-D4YK18I3.mjs → document-translation-CeJFGMoy.mjs} +1 -1
- package/dist/{document-translation-CIPGsZD8.d.mts → document-translation-D50BnvVh.d.mts} +100 -100
- package/dist/document-translation.d.mts +1 -1
- package/dist/document-translation.mjs +2 -2
- package/dist/{hosted-app-CzMRiEgY.mjs → hosted-app-BPU4OR41.mjs} +31 -9
- package/dist/hosted-app.d.mts +25 -2
- package/dist/hosted-app.mjs +7 -2
- package/dist/hosted-contract-CcRnb51e.mjs +234 -0
- package/dist/hosted-contract.d.mts +25 -0
- package/dist/hosted-contract.mjs +7 -0
- package/dist/index.d.mts +25 -2686
- package/dist/index.mjs +2114 -1098
- package/dist/{local-download-profiles-B30HrSO2.d.mts → local-download-profiles-BL_rtlVM.d.mts} +1 -1
- package/dist/local-download-profiles.d.mts +2 -2
- package/dist/local-download-profiles.mjs +1 -1
- package/dist/markdown-facts.d.mts +1 -1
- package/dist/markdown-facts.mjs +1 -1
- package/dist/{markdown-reading-CgXuXp6s.d.mts → markdown-reading-LUdqfbxH.d.mts} +1 -1
- package/dist/{markdown-reading-DdcB7run.mjs → markdown-reading-mG4OAbDO.mjs} +1 -1
- package/dist/markdown-reading.d.mts +2 -2
- package/dist/markdown-reading.mjs +2 -2
- package/dist/{notifications-AKAcmeKT.mjs → notifications-C2bxAzX_.mjs} +13 -2
- package/dist/{notifications-HYH-lklF.d.mts → notifications-CgJlLgLO.d.mts} +34 -8
- package/dist/notifications.d.mts +2 -2
- package/dist/notifications.mjs +3 -3
- package/dist/{openspec-annotations-CeXd0JIZ.d.mts → openspec-annotations-C58Aoi-V.d.mts} +2 -2
- package/dist/{openspec-annotations-D_6Pz7k4.mjs → openspec-annotations-DCfqBMal.mjs} +2 -2
- package/dist/openspec-annotations.d.mts +3 -3
- package/dist/openspec-annotations.mjs +3 -3
- package/dist/openspec-compat.d.mts +18 -11
- package/dist/openspec-compat.mjs +22 -14
- package/dist/{openspec-projection-B8s-KWQ7.mjs → openspec-projection-DCgToo0Q.mjs} +2 -2
- package/dist/{openspec-projection-BphzrwYP.d.mts → openspec-projection-RohNZI4p.d.mts} +3 -3
- package/dist/openspec-projection.d.mts +5 -5
- package/dist/openspec-projection.mjs +4 -4
- package/dist/opsx-display-path.d.mts +1 -1
- package/dist/opsx-display-path.mjs +1 -1
- package/dist/{opsx-entity-CFUCqev8.d.mts → opsx-entity-D7kDQsze.d.mts} +2 -2
- package/dist/opsx-entity.d.mts +7 -3
- package/dist/opsx-entity.mjs +1 -1
- package/dist/{opsx-schema-detail-DXKXqMQa.d.mts → opsx-schema-detail-BxDXEwDY.d.mts} +2 -2
- package/dist/opsx-schema-detail.d.mts +8 -4
- package/dist/opsx-schema-detail.mjs +1 -1
- package/dist/opsx-types-BJ8AjhrS.d.mts +3642 -0
- package/dist/opsx-workflows-BvnJqjfL.d.mts +36 -0
- package/dist/opsx-workflows-Ccc6U7IR.mjs +89 -0
- package/dist/opsx-workflows.d.mts +2 -0
- package/dist/opsx-workflows.mjs +3 -0
- package/dist/planning-cli-projection-DIRxeq2H.mjs +421 -0
- package/dist/planning-cli-projection.d.mts +25 -0
- package/dist/planning-cli-projection.mjs +9 -0
- package/dist/schemas-BFroDdOC.mjs +287 -0
- package/dist/{schemas-CNKCVc7m.d.mts → schemas-DS-JqAJ2.d.mts} +498 -52
- package/dist/{sounds-J6z7o4mt.d.mts → sounds-qhRgCy0F.d.mts} +5 -5
- package/dist/sounds.d.mts +1 -1
- package/dist/sounds.mjs +1 -1
- package/dist/spec-catalog-ZGuiZ4CE.mjs +901 -0
- package/dist/spec-catalog.d.mts +25 -0
- package/dist/spec-catalog.mjs +6 -0
- package/dist/store-mutation-protocol-5tfs-RmD.d.mts +727 -0
- package/dist/store-mutation-protocol-CSwD-xPO.mjs +63 -0
- package/dist/store-mutation-protocol.d.mts +2 -0
- package/dist/store-mutation-protocol.mjs +4 -0
- package/dist/{store-types-Cu41QSww.mjs → store-types-DlX1REBg.mjs} +39 -18
- package/dist/{store-types-BiE-Ey--.d.mts → store-types-b554wAQJ.d.mts} +22 -12
- package/dist/store-types.d.mts +6 -2
- package/dist/store-types.mjs +2 -2
- package/dist/task-progress-B_IWVXjA.mjs +216 -0
- package/dist/task-progress.d.mts +7 -4
- package/dist/task-progress.mjs +3 -3
- package/dist/{terminal-audio-TBoU2jSK.mjs → terminal-audio-d6KItUeJ.mjs} +1 -1
- package/dist/{terminal-audio-BeUXlQgj.d.mts → terminal-audio-tXDm_EDe.d.mts} +2 -2
- package/dist/terminal-audio.d.mts +2 -2
- package/dist/terminal-audio.mjs +2 -2
- package/dist/terminal-control.d.mts +2 -2
- package/dist/terminal-control.mjs +1 -1
- package/dist/{terminal-invocation-ClVh0HeM.d.mts → terminal-invocation-BOvxThym.d.mts} +203 -84
- package/dist/terminal-invocation.d.mts +1 -1
- package/dist/terminal-invocation.mjs +1 -1
- package/dist/terminal-theme.d.mts +1 -1
- package/dist/terminal-theme.mjs +1 -1
- package/dist/translation-language-pair.d.mts +1 -1
- package/dist/translation-language-pair.mjs +1 -1
- package/dist/{translator-D6t_LEVN.d.mts → translator-NKv1wo3o.d.mts} +208 -208
- package/dist/translator.d.mts +1 -1
- package/dist/translator.mjs +1 -1
- package/package.json +55 -7
- package/dist/hosted-app-DHs94gIR.d.mts +0 -43
- package/dist/opsx-types-2rgsQHsB.d.mts +0 -402
- package/dist/task-progress-BHn4bCoD.d.mts +0 -24
- package/dist/task-progress-DhHYHnTv.mjs +0 -70
- /package/dist/{local-download-profiles-kpNEcdF_.mjs → local-download-profiles-odzOdf96.mjs} +0 -0
- /package/dist/{markdown-facts-BBCTe_qq.mjs → markdown-facts-BUqLvFRf.mjs} +0 -0
- /package/dist/{markdown-facts-Dk7FKaaT.d.mts → markdown-facts-CCw3QdbM.d.mts} +0 -0
- /package/dist/{opsx-display-path-CUCOrHcM.mjs → opsx-display-path-D13XV20v.mjs} +0 -0
- /package/dist/{opsx-display-path-cIMKa1aw.d.mts → opsx-display-path-RXS5JTbV.d.mts} +0 -0
- /package/dist/{opsx-entity-c7m5CbgU.mjs → opsx-entity-Cn_Zu1J3.mjs} +0 -0
- /package/dist/{opsx-schema-detail-BH82X6aU.mjs → opsx-schema-detail-Beis4Ba2.mjs} +0 -0
- /package/dist/{sounds-CB3ubSnj.mjs → sounds-NXwveBqq.mjs} +0 -0
- /package/dist/{terminal-control-VpTuh17B.mjs → terminal-control-BSAlOTs8.mjs} +0 -0
- /package/dist/{terminal-invocation-C2MCNBhK.mjs → terminal-invocation-0p-j2eSP.mjs} +0 -0
- /package/dist/{terminal-theme-cevliJw2.mjs → terminal-theme-B8iRttTR.mjs} +0 -0
- /package/dist/{terminal-theme-hvQjTsNh.d.mts → terminal-theme-m3MSGJ7U.d.mts} +0 -0
- /package/dist/{translation-language-pair-DZ_WNp_M.mjs → translation-language-pair-Bbhk-LAC.mjs} +0 -0
- /package/dist/{translation-language-pair-DmaOfHqD.d.mts → translation-language-pair-C18GPATW.d.mts} +0 -0
- /package/dist/{translator-DbUT1Ayn.mjs → translator-Cjtv4tya.mjs} +0 -0
package/dist/index.mjs
CHANGED
|
@@ -1,142 +1,60 @@
|
|
|
1
|
-
import { a as
|
|
2
|
-
import { t as
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import { a as
|
|
6
|
-
import { a as
|
|
7
|
-
import { i as
|
|
8
|
-
import {
|
|
9
|
-
import { a as
|
|
10
|
-
import { _ as
|
|
11
|
-
import { n as
|
|
12
|
-
import { a as
|
|
13
|
-
import { n as
|
|
14
|
-
import { a as
|
|
15
|
-
import {
|
|
16
|
-
import { n as
|
|
17
|
-
import {
|
|
18
|
-
import { n as
|
|
19
|
-
import { i as
|
|
20
|
-
import {
|
|
21
|
-
import {
|
|
1
|
+
import { _ as isTextLikeMime, a as DeltaSpecSchema, c as ScenarioStepSchema, d as TrackedTaskProgressSchema, f as FILE_PREVIEW_KINDS, g as isTextLikeFile, h as isPreviewableFile, i as DeltaSchema, l as SpecSchema, m as inferFilePreviewKind, n as ChangeSchema, o as RequirementSchema, p as inferFileMime, r as DeltaOperationType, s as ScenarioStepKeywordSchema, t as ChangeFileSchema, u as TaskSchema } from "./schemas-BFroDdOC.mjs";
|
|
2
|
+
import { a as normalizeOpsxEntityPath, c as parseOpsxEntityMetadata, i as isOpsxGlobPattern, n as getOpsxEntityMetadataPath, o as opsxGlobToRegex, r as getOpsxEntityRootRelativePath, s as opsxPathMatchesPattern, t as buildOpsxEntityDetail } from "./opsx-entity-Cn_Zu1J3.mjs";
|
|
3
|
+
import { t as parseOpsxSchemaDetail } from "./opsx-schema-detail-Beis4Ba2.mjs";
|
|
4
|
+
import { n as toMarkdownFactKind, t as parseMarkdownFacts } from "./markdown-facts-BUqLvFRf.mjs";
|
|
5
|
+
import { a as getMarkdownAnnotation, c as getMarkdownHeadingEnd, d as trimMarkdownSlice, i as createMarkdownReadingDocumentFromFacts, l as getMarkdownHeadingFacts, n as buildMarkdownParentMap, o as getMarkdownAnnotationsForFact, r as createMarkdownReadingDocument, s as getMarkdownFactSpan, t as MarkdownReadingPluginRegistry, u as sortMarkdownReadingPlugins } from "./markdown-reading-mG4OAbDO.mjs";
|
|
6
|
+
import { a as getOpenSpecAnnotationsForFact, i as getOpenSpecAnnotation, n as annotateOpenSpecMarkdown, o as openSpecAnnotationRules, r as builtinOpenSpecReadingPlugin, t as annotateOpenSpecFacts } from "./openspec-annotations-DCfqBMal.mjs";
|
|
7
|
+
import { a as getOpenSpecReadingSections, c as projectOpenSpecMarkdown, i as getOpenSpecProjectionAnnotation, n as OPEN_SPEC_SPEC_PROJECTION_ID, o as parseOpenSpecMarkdownToSpec, r as createOpenSpecReadingPlugin, s as projectAnnotatedOpenSpecToSpec, t as OPEN_SPEC_READING_SECTIONS_PROJECTION_ID } from "./openspec-projection-DCgToo0Q.mjs";
|
|
8
|
+
import { a as isMarkdownTaskSourcePath, c as toggleMarkdownTask, i as deriveTrackedTaskPhase, n as createDocumentChecklistSummary, o as parseMarkdownTasks, r as createTrackedTaskProgress, s as projectTaskProjectionsFromMarkdownFiles, t as createApplyInstructionProgress } from "./task-progress-B_IWVXjA.mjs";
|
|
9
|
+
import { a as CliRootSchema, i as CliReferenceIndexEntrySchema, n as CliDiagnosticSchema, o as CliRootSourceSchema, r as CliDoctorReferenceEntrySchema, t as CliDiagnosticFailureSchema } from "./common-D8YUK9dA.mjs";
|
|
10
|
+
import { A as CliSchemasSchema, B as CliStoreCleanupSchema, C as CliArtifactInstructionsSuccessSchema, F as CliValidateSchema, G as CliStoreSchema, H as CliStoreDoctorSchema, I as CliWorkflowStatusSchema, K as parseCliCommandResult, L as CliWorkflowStatusSuccessSchema, M as CliShowSpecSchema, N as CliSpecListSchema, R as CliContextSchema, S as CliArtifactInstructionsSchema, T as CliChangeListSchema, U as CliStoreListSchema, V as CliStoreDoctorEntrySchema, W as CliStoreMutationSchema, _ as OpenSpecCliContractExecutor, a as SpecDocumentProjectionSchema, b as CliApplyInstructionsSuccessSchema, c as createStaticSpecCatalogOwnedProjection, d as getSpecCatalogEntry, f as isReferencedSpecIdentity, g as specRoutePath, h as specIdentityKey, i as SpecCommandEvidenceSchema, j as CliShowSpecDocumentSchema, l as createStaticSpecCatalogReferenceProjection, m as specIdentityFromRoute, n as ReferencedSpecIdentitySchema, o as SpecIdentitySchema, p as mergeSpecCatalog, r as SpecCatalogSchema, s as buildSpecCatalog, t as OwnedSpecIdentitySchema, u as createStaticSpecCatalogReferenceSource, x as CliArchiveSchema, y as CliApplyInstructionsSchema, z as CliDoctorSchema } from "./spec-catalog-ZGuiZ4CE.mjs";
|
|
11
|
+
import { a as CliProjectionInvalidationCauseSchema, d as toCliProjectionCommandEvidence, f as toCliProjectionFailure, i as CliProjectionFailureSchema, l as cliProjectionStates, n as CliProjectionCommandError, o as CliProjectionNoticeSchema, p as toCliProjectionNotice, r as CliProjectionCommandEvidenceSchema, s as CliProjectionStateNameSchema, t as CliJsonValueSchema, u as createCliProjectionStateSchema } from "./cli-projection-vO3-FJg5.mjs";
|
|
12
|
+
import { $ as shouldShowTranslationEngineInstallGate, A as TranslationEngineIdSchema, B as TranslationLocalLlamaSettingsUpdateSchema, C as TranslationDownloadGroupPlanSchema, D as TranslationEngineDependencyStatusSchema, E as TranslationEngineDependencyStateSchema, F as TranslationEngineRuntimeStateSchema, G as createTranslationEngineLifecycleStatus, H as TranslationLocalSettingsUpdateSchema, I as TranslationEngineRuntimeStatusSchema, J as getTranslationEngineManifest, K as getManagedLocalTranslationEngineManifest, L as TranslationLocalCt2SettingsSchema, M as TranslationEngineInstallLogStreamSchema, N as TranslationEngineLifecycleEventSchema, O as TranslationEngineGlobalSettingsSchema, P as TranslationEngineLifecycleStatusSchema, Q as isTranslationEngineRuntimeReady, R as TranslationLocalCt2SettingsUpdateSchema, S as TranslationDownloadFilePlanSchema, T as TranslationEngineAssetStatusSchema, U as TranslationOpenAISettingsSchema, V as TranslationLocalSettingsSchema, W as TranslationOpenAISettingsUpdateSchema, X as isManagedLocalTranslationEngineId, Y as isDirectionalManagedLocalTranslationEngineId, Z as isTranslationEngineDependencyReady, _ as SERVICE_TRANSLATION_ENGINE_IDS, a as LocalModelAssetLogSchema, b as TRANSLATION_ENGINE_MANIFESTS, c as LocalModelDownloadStatusSchema, d as LocalModelProfileLoadStateSchema, et as isBatchTranslationAbort, f as LocalModelProfileManifestFileSchema, g as ManagedLocalCatalogSourceSchema, h as LocalModelProfileStatusSchema, i as DEFAULT_TRANSLATION_ENGINE_ID, j as TranslationEngineInstallLogEventSchema, k as TranslationEngineGlobalSettingsUpdateSchema, l as LocalModelLifecycleFileStateSchema, m as LocalModelProfileManifestSchema, n as BatchTranslateInputSchema, nt as runControlledTranslationTask, o as LocalModelAssetPlanSnapshotSchema, p as LocalModelProfileManifestGroupSchema, q as getTranslationEngineLifecycleMessage, s as LocalModelAssetStateSchema, t as BatchTranslateEventSchema, tt as normalizeBatchTranslationError, u as LocalModelLifecycleGroupStateSchema, v as ServiceTranslationEngineIdSchema, w as TranslationEngineAssetStateSchema, x as TRANSLATOR_CONTRACT_VERSION, y as TRANSLATION_ENGINE_IDS, z as TranslationLocalLlamaSettingsSchema } from "./translator-Cjtv4tya.mjs";
|
|
13
|
+
import { a as DocumentTranslationDisplayModeSchema, c as TRANSLATION_CACHE_POLICY_VERSION, d as TranslationCacheSettingsSchema, f as TranslationCacheSettingsUpdateSchema, g as TranslationEngineProjectSettingsUpdateSchema, h as TranslationEngineProjectSettingsSchema, i as DocumentTranslationConfigUpdateSchema, l as TranslationCacheEntrySchema, m as TranslationCacheWriteInputSchema, n as DOCUMENT_TRANSLATION_DISPLAY_MODES, o as MAX_TRANSLATION_CACHE_ENTRY_LIMIT, p as TranslationCacheStatsSchema, r as DocumentTranslationConfigSchema, s as MIN_TRANSLATION_CACHE_ENTRY_LIMIT, t as DEFAULT_TRANSLATION_CACHE_ENTRY_LIMIT, u as TranslationCacheReadInputSchema } from "./document-translation-CeJFGMoy.mjs";
|
|
14
|
+
import { _ as customHashFromSoundId, a as CustomSoundHashSchema, b as soundIdFromCustomHash, c as CustomSoundMetadataSchema, f as LEGACY_SOUND_ID_MAP, g as SoundVolumeSchema, h as SoundIdSchema, i as CUSTOM_SOUND_ADD_VALUE, l as DEFAULT_BELL_SOUND_ID, m as SoundConfigIdSchema, n as BUILTIN_SOUND_OPTIONS, o as CustomSoundIdSchema, p as SILENT_SOUND_ID, r as BuiltinSoundIdSchema, s as CustomSoundMetadataFileSchema, t as BUILTIN_SOUND_IDS, u as DEFAULT_NOTIFICATION_SOUND_ID, v as getBuiltinSoundUrl, y as normalizeLegacySoundId } from "./sounds-NXwveBqq.mjs";
|
|
15
|
+
import { n as terminalNotificationEventToPublishInput, t as TerminalControlParser } from "./terminal-control-BSAlOTs8.mjs";
|
|
16
|
+
import { a as NotificationPublishInputSchema, c as NotificationSoundSchema, h as groupNotifications, i as NotificationGroupKeySchema, l as NotificationSourceSchema, m as getNotificationGroupLabel, n as NOTIFICATION_SOUND_VALUES, o as NotificationRecordSchema, p as getNotificationGroupKey, r as NotificationActionSchema, s as NotificationSettingsSchema, t as NOTIFICATION_SOUND_OPTIONS, u as TerminalNotificationParser } from "./notifications-C2bxAzX_.mjs";
|
|
17
|
+
import { n as TERMINAL_BELL_SOUND_VALUES, r as TerminalBellSoundSchema, t as TERMINAL_BELL_SOUND_OPTIONS } from "./terminal-audio-d6KItUeJ.mjs";
|
|
18
|
+
import { a as TERMINAL_THEME_VALUES, i as TERMINAL_THEME_MODE_VALUES, n as DEFAULT_TERMINAL_LIGHT_THEME, r as DEFAULT_TERMINAL_THEME_MODE, t as DEFAULT_TERMINAL_DARK_THEME } from "./terminal-theme-B8iRttTR.mjs";
|
|
19
|
+
import { i as selectLocalDownloadGroup, n as buildLocalDownloadPlanFromRepositoryFiles, r as buildLocalDownloadPlanFromRuntimeProfileFiles, t as LOCAL_MODEL_PROFILE_DEFINITIONS } from "./local-download-profiles-odzOdf96.mjs";
|
|
20
|
+
import { n as inferLocalDirectionalModelLanguagePair, t as checkLocalDirectionalModelLanguagePair } from "./translation-language-pair-Bbhk-LAC.mjs";
|
|
21
|
+
import { C as TemplateContentMapSchema, E as isGlobPattern, S as SchemaResolutionSchema, T as TemplatesSchema, _ as OpsxConfigBundleSchema, a as PlanningCliProjectionDataSchema, b as SchemaDetailSchema, c as ApplyInstructionsContextFilesSchema, d as ApplyTaskSchema, f as ArtifactInstructionsSchema, g as OpsxCliEvidenceSchema, h as DependencyInfoSchema, i as EnvironmentGlobalProjectionStateSchema, l as ApplyInstructionsProjectionSchema, m as ChangeStatusSchema, n as EnvironmentGlobalFileProjectionStateSchema, o as PlanningCliProjectionSelectorSchema, p as ArtifactStatusSchema, r as EnvironmentGlobalProjectionDataSchema, s as PlanningCliProjectionStateSchema, t as EnvironmentGlobalFileProjectionDataSchema, u as ApplyInstructionsSchema, v as OpsxStatusEvidenceSchema, w as TemplateContentSchema, x as SchemaInfoSchema, y as SchemaArtifactSchema } from "./planning-cli-projection-DIRxeq2H.mjs";
|
|
22
|
+
import { a as OPSX_CORE_PROFILE_WORKFLOWS, c as OPSX_WORKFLOW_TO_SKILL_DIR, i as OPSX_COMMAND_CAPABLE_WORKFLOWS, l as isOpsxCoreWorkflowSelection, n as OPSX_ARTIFACT_INPUT_ACTIONS, o as OPSX_TEXT_INPUT_ACTIONS, r as OPSX_CHANGE_INPUT_ACTIONS, s as OPSX_WORKFLOW_LABELS, t as OPSX_ALL_WORKFLOWS } from "./opsx-workflows-Ccc6U7IR.mjs";
|
|
23
|
+
import { a as DASHBOARD_METRIC_KEYS, c as DashboardSummaryProjectionSchema, i as DashboardSummaryProjectionStateSchema, l as DashboardTrendsProjectionSchema, n as DashboardSummaryInvalidationCauseSchema, o as DashboardGitSnapshotSchema, r as DashboardSummaryInvalidationSchema, s as DashboardOverviewSchema, t as DashboardSummaryIdentitySchema } from "./dashboard-summary-transport-D2VBAIxg.mjs";
|
|
24
|
+
import { i as StoreMutationStartResponseSchema, n as StoreMutationResultSchema, r as StoreMutationSchema, t as StoreMutationLifecycleEventSchema } from "./store-mutation-protocol-CSwD-xPO.mjs";
|
|
25
|
+
import { a as StoreListResultSchema, n as StoreDoctorResultSchema, o as classifyStoreCliResult, s as toStoreFeatureResult } from "./store-types-DlX1REBg.mjs";
|
|
26
|
+
import { S as isTerminalMutationStatus, b as asEnvUri, t as HOSTED_SHELL_PROTOCOL_VERSION, v as OPENSPECUI_RUNTIME_CAPABILITIES, x as hasCapability } from "./hosted-contract-CcRnb51e.mjs";
|
|
27
|
+
import { a as buildHostedLaunchUrl, c as isSupportedEmbeddedUiUrl, d as resolveHostedAppBaseUrl, i as buildEmbeddedUiLaunchUrl, l as normalizeEmbeddedUiUrl, n as OFFICIAL_APP_BASE_URL, o as isBackendHealthRuntimeMetadata, r as buildBackendHealthPayload, s as isHostedBackendHealthResponse, u as normalizeHostedAppBaseUrl } from "./hosted-app-BPU4OR41.mjs";
|
|
28
|
+
import { n as toOpsxDisplayPath, t as VIRTUAL_PROJECT_DIRNAME } from "./opsx-display-path-D13XV20v.mjs";
|
|
29
|
+
import { a as TerminalInvocationSettingsSchema, c as TerminalSpawnCommandSchema, f as quoteTerminalShellArg, g as resolveTerminalShellDefaults, h as renderTerminalSpawnCommandLine, i as TerminalCommandFieldSchema, n as TERMINAL_COMMAND_FIELD_TYPE_VALUES, o as TerminalShellProfileSchema, p as renderTerminalCommandArgs, r as TERMINAL_SHELL_QUOTE_STYLE_VALUES, s as TerminalShellQuoteStyleSchema, t as BUILTIN_TERMINAL_SPAWN_COMMANDS, u as getTerminalCommandDefaultValues } from "./terminal-invocation-0p-j2eSP.mjs";
|
|
30
|
+
import { mkdir, writeFile } from "fs/promises";
|
|
22
31
|
import { dirname, join } from "path";
|
|
23
|
-
import {
|
|
24
|
-
import { mkdir as mkdir$1, readFile
|
|
25
|
-
import { basename, dirname as dirname$1, join as join$1, matchesGlob, relative, resolve, sep } from "node:path";
|
|
32
|
+
import { isMap, parseDocument } from "yaml";
|
|
33
|
+
import { lstat, mkdir as mkdir$1, readFile, readdir, realpath, rm, stat, writeFile as writeFile$1 } from "node:fs/promises";
|
|
34
|
+
import { basename, dirname as dirname$1, isAbsolute, join as join$1, matchesGlob, posix, relative, resolve, sep, win32 } from "node:path";
|
|
26
35
|
import { existsSync, lstatSync, readFileSync, realpathSync } from "node:fs";
|
|
36
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
27
37
|
import { z } from "zod";
|
|
28
38
|
import { EventEmitter } from "events";
|
|
29
39
|
import { watch } from "fs";
|
|
30
40
|
import { exec, execFile, spawn } from "child_process";
|
|
31
41
|
import { promisify } from "util";
|
|
32
|
-
import { homedir } from "node:os";
|
|
42
|
+
import { homedir, platform } from "node:os";
|
|
43
|
+
import { createHash, randomBytes } from "node:crypto";
|
|
33
44
|
|
|
34
|
-
//#region src/
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
[".ini", "text/plain"],
|
|
48
|
-
[".cfg", "text/plain"],
|
|
49
|
-
[".conf", "text/plain"],
|
|
50
|
-
[".html", "text/html"],
|
|
51
|
-
[".htm", "text/html"],
|
|
52
|
-
[".svg", "image/svg+xml"],
|
|
53
|
-
[".png", "image/png"],
|
|
54
|
-
[".jpg", "image/jpeg"],
|
|
55
|
-
[".jpeg", "image/jpeg"],
|
|
56
|
-
[".gif", "image/gif"],
|
|
57
|
-
[".webp", "image/webp"],
|
|
58
|
-
[".avif", "image/avif"],
|
|
59
|
-
[".bmp", "image/bmp"],
|
|
60
|
-
[".mp3", "audio/mpeg"],
|
|
61
|
-
[".wav", "audio/wav"],
|
|
62
|
-
[".ogg", "audio/ogg"],
|
|
63
|
-
[".oga", "audio/ogg"],
|
|
64
|
-
[".m4a", "audio/mp4"],
|
|
65
|
-
[".aac", "audio/aac"],
|
|
66
|
-
[".flac", "audio/flac"],
|
|
67
|
-
[".mp4", "video/mp4"],
|
|
68
|
-
[".m4v", "video/mp4"],
|
|
69
|
-
[".webm", "video/webm"],
|
|
70
|
-
[".mov", "video/quicktime"],
|
|
71
|
-
[".mkv", "video/x-matroska"],
|
|
72
|
-
[".ogv", "video/ogg"],
|
|
73
|
-
[".pdf", "application/pdf"],
|
|
74
|
-
[".js", "text/javascript"],
|
|
75
|
-
[".jsx", "text/javascript"],
|
|
76
|
-
[".ts", "text/typescript"],
|
|
77
|
-
[".tsx", "text/typescript"],
|
|
78
|
-
[".css", "text/css"],
|
|
79
|
-
[".sh", "text/plain"],
|
|
80
|
-
[".py", "text/plain"],
|
|
81
|
-
[".rs", "text/plain"],
|
|
82
|
-
[".go", "text/plain"],
|
|
83
|
-
[".java", "text/plain"],
|
|
84
|
-
[".c", "text/plain"],
|
|
85
|
-
[".cc", "text/plain"],
|
|
86
|
-
[".cpp", "text/plain"],
|
|
87
|
-
[".h", "text/plain"],
|
|
88
|
-
[".hpp", "text/plain"]
|
|
89
|
-
]);
|
|
90
|
-
const TEXT_MIME_PREFIXES = ["text/"];
|
|
91
|
-
const TEXT_MIME_VALUES = new Set([
|
|
92
|
-
"application/json",
|
|
93
|
-
"application/yaml",
|
|
94
|
-
"application/toml",
|
|
95
|
-
"application/xml",
|
|
96
|
-
"image/svg+xml"
|
|
97
|
-
]);
|
|
98
|
-
const FILE_PREVIEW_KINDS = [
|
|
99
|
-
"markdown",
|
|
100
|
-
"html",
|
|
101
|
-
"image",
|
|
102
|
-
"audio",
|
|
103
|
-
"video",
|
|
104
|
-
"pdf",
|
|
105
|
-
"text",
|
|
106
|
-
"none"
|
|
107
|
-
];
|
|
108
|
-
function getExtension(path) {
|
|
109
|
-
const normalized = path.replace(/\\/g, "/");
|
|
110
|
-
const name = normalized.split("/").pop() ?? normalized;
|
|
111
|
-
const dotIndex = name.lastIndexOf(".");
|
|
112
|
-
if (dotIndex < 0) return "";
|
|
113
|
-
return name.slice(dotIndex).toLowerCase();
|
|
114
|
-
}
|
|
115
|
-
function inferFileMime(path) {
|
|
116
|
-
return MIME_BY_EXTENSION.get(getExtension(path)) ?? null;
|
|
117
|
-
}
|
|
118
|
-
function inferFilePreviewKind(path, mime) {
|
|
119
|
-
const resolvedMime = mime ?? inferFileMime(path);
|
|
120
|
-
if (!resolvedMime) return "none";
|
|
121
|
-
if (resolvedMime === "text/markdown") return "markdown";
|
|
122
|
-
if (resolvedMime === "text/html") return "html";
|
|
123
|
-
if (resolvedMime.startsWith("image/")) return resolvedMime === "image/svg+xml" ? "text" : "image";
|
|
124
|
-
if (resolvedMime.startsWith("audio/")) return "audio";
|
|
125
|
-
if (resolvedMime.startsWith("video/")) return "video";
|
|
126
|
-
if (resolvedMime === "application/pdf") return "pdf";
|
|
127
|
-
if (isTextLikeMime(resolvedMime)) return "text";
|
|
128
|
-
return "none";
|
|
129
|
-
}
|
|
130
|
-
function isTextLikeMime(mime) {
|
|
131
|
-
if (!mime) return false;
|
|
132
|
-
if (TEXT_MIME_VALUES.has(mime)) return true;
|
|
133
|
-
return TEXT_MIME_PREFIXES.some((prefix) => mime.startsWith(prefix));
|
|
134
|
-
}
|
|
135
|
-
function isTextLikeFile(path, mime) {
|
|
136
|
-
return isTextLikeMime(mime ?? inferFileMime(path));
|
|
137
|
-
}
|
|
138
|
-
function isPreviewableFile(path, mime) {
|
|
139
|
-
return inferFilePreviewKind(path, mime) !== "none";
|
|
45
|
+
//#region src/entity-id.ts
|
|
46
|
+
/** Return one path-safe OpenSpec entity id or reject it before filesystem access. */
|
|
47
|
+
function requireCanonicalOpenSpecEntityId(value, field) {
|
|
48
|
+
if (value.length === 0 || value !== value.trim() || value === "." || value === ".." || value.includes("/") || value.includes("\\") || value.includes("\0")) throw new Error(`Invalid ${field}: expected one canonical path segment.`);
|
|
49
|
+
return value;
|
|
50
|
+
}
|
|
51
|
+
/** Return one normalized entity-relative path or glob without parent traversal. */
|
|
52
|
+
function requireOpenSpecEntityRelativePath(value, field) {
|
|
53
|
+
const normalizedSeparators = value.replace(/\\/g, "/");
|
|
54
|
+
if (normalizedSeparators.length === 0 || normalizedSeparators.includes("\0") || normalizedSeparators.startsWith("/") || /^[A-Za-z]:/.test(normalizedSeparators) || normalizedSeparators.split("/").includes("..")) throw new Error(`Invalid ${field}: expected an entity-relative path.`);
|
|
55
|
+
const normalizedPath = normalizedSeparators.split("/").filter((segment) => segment.length > 0 && segment !== ".").join("/");
|
|
56
|
+
if (!normalizedPath) throw new Error(`Invalid ${field}: expected an entity-relative path.`);
|
|
57
|
+
return normalizedPath;
|
|
140
58
|
}
|
|
141
59
|
|
|
142
60
|
//#endregion
|
|
@@ -206,7 +124,11 @@ var MarkdownParser = class {
|
|
|
206
124
|
});
|
|
207
125
|
}
|
|
208
126
|
}
|
|
209
|
-
const
|
|
127
|
+
const taskProjections = projectTaskProjectionsFromMarkdownFiles(tasksContent.length > 0 ? [{
|
|
128
|
+
path: "tasks.md",
|
|
129
|
+
type: "file",
|
|
130
|
+
content: tasksContent
|
|
131
|
+
}] : []);
|
|
210
132
|
const deltasFromDeltaSpecs = this.parseDeltasFromDeltaSpecs(options?.deltaSpecs);
|
|
211
133
|
const deltasFromWhatChanges = this.parseDeltasFromWhatChanges(whatChanges);
|
|
212
134
|
const combinedDeltas = deltasFromDeltaSpecs.length > 0 ? deltasFromDeltaSpecs : deltas;
|
|
@@ -217,11 +139,8 @@ var MarkdownParser = class {
|
|
|
217
139
|
why: why.trim(),
|
|
218
140
|
whatChanges: whatChanges.trim(),
|
|
219
141
|
deltas: finalDeltas,
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
total: tasks.length,
|
|
223
|
-
completed: tasks.filter((t) => t.completed).length
|
|
224
|
-
},
|
|
142
|
+
trackedTaskProgress: taskProjections.trackedTaskProgress,
|
|
143
|
+
documentChecklistSummary: taskProjections.documentChecklistSummary,
|
|
225
144
|
design: options?.design,
|
|
226
145
|
deltaSpecs: options?.deltaSpecs
|
|
227
146
|
};
|
|
@@ -381,171 +300,6 @@ var MarkdownParser = class {
|
|
|
381
300
|
}
|
|
382
301
|
};
|
|
383
302
|
|
|
384
|
-
//#endregion
|
|
385
|
-
//#region src/reactive-fs/reactive-state.ts
|
|
386
|
-
/**
|
|
387
|
-
* 全局的 AsyncLocalStorage,用于在异步调用链中传递 ReactiveContext
|
|
388
|
-
* 这是实现依赖收集的核心机制
|
|
389
|
-
*/
|
|
390
|
-
const contextStorage = new AsyncLocalStorage();
|
|
391
|
-
/**
|
|
392
|
-
* 响应式状态类,类似 Signal.State
|
|
393
|
-
*
|
|
394
|
-
* 核心机制:
|
|
395
|
-
* - get() 时自动注册到当前 ReactiveContext 的依赖列表
|
|
396
|
-
* - set() 时如果值变化,通知所有依赖的 Context
|
|
397
|
-
*/
|
|
398
|
-
var ReactiveState = class {
|
|
399
|
-
currentValue;
|
|
400
|
-
equals;
|
|
401
|
-
/** 所有依赖此状态的 Context */
|
|
402
|
-
subscribers = /* @__PURE__ */ new Set();
|
|
403
|
-
constructor(initialValue, options) {
|
|
404
|
-
this.currentValue = initialValue;
|
|
405
|
-
this.equals = options?.equals ?? ((a, b) => a === b);
|
|
406
|
-
}
|
|
407
|
-
/**
|
|
408
|
-
* 获取当前值
|
|
409
|
-
* 如果在 ReactiveContext 中调用,会自动注册依赖
|
|
410
|
-
*/
|
|
411
|
-
get() {
|
|
412
|
-
const context = contextStorage.getStore();
|
|
413
|
-
if (context) {
|
|
414
|
-
context.track(this);
|
|
415
|
-
this.subscribers.add(context);
|
|
416
|
-
}
|
|
417
|
-
return this.currentValue;
|
|
418
|
-
}
|
|
419
|
-
/**
|
|
420
|
-
* 设置新值
|
|
421
|
-
* 如果值变化,通知所有订阅者
|
|
422
|
-
* @returns 是否发生了变化
|
|
423
|
-
*/
|
|
424
|
-
set(newValue) {
|
|
425
|
-
if (this.equals(this.currentValue, newValue)) return false;
|
|
426
|
-
this.currentValue = newValue;
|
|
427
|
-
for (const context of this.subscribers) context.notifyChange();
|
|
428
|
-
return true;
|
|
429
|
-
}
|
|
430
|
-
/**
|
|
431
|
-
* 取消订阅
|
|
432
|
-
* 当 Context 销毁时调用
|
|
433
|
-
*/
|
|
434
|
-
unsubscribe(context) {
|
|
435
|
-
this.subscribers.delete(context);
|
|
436
|
-
}
|
|
437
|
-
/**
|
|
438
|
-
* 获取当前订阅者数量(用于调试)
|
|
439
|
-
*/
|
|
440
|
-
get subscriberCount() {
|
|
441
|
-
return this.subscribers.size;
|
|
442
|
-
}
|
|
443
|
-
};
|
|
444
|
-
|
|
445
|
-
//#endregion
|
|
446
|
-
//#region src/reactive-fs/reactive-context.ts
|
|
447
|
-
function createPromiseWithResolvers() {
|
|
448
|
-
let resolve$1;
|
|
449
|
-
let reject;
|
|
450
|
-
return {
|
|
451
|
-
promise: new Promise((res, rej) => {
|
|
452
|
-
resolve$1 = res;
|
|
453
|
-
reject = rej;
|
|
454
|
-
}),
|
|
455
|
-
resolve: resolve$1,
|
|
456
|
-
reject
|
|
457
|
-
};
|
|
458
|
-
}
|
|
459
|
-
/**
|
|
460
|
-
* 响应式上下文,管理依赖收集和变更通知
|
|
461
|
-
*
|
|
462
|
-
* 核心机制:
|
|
463
|
-
* - 在 stream() 中执行任务时,通过 AsyncLocalStorage 传递 this
|
|
464
|
-
* - 任务中的所有 ReactiveState.get() 调用都会自动注册依赖
|
|
465
|
-
* - 当任何依赖变更时,重新执行任务并 yield 新结果
|
|
466
|
-
*/
|
|
467
|
-
var ReactiveContext = class {
|
|
468
|
-
/** 当前追踪的依赖 */
|
|
469
|
-
dependencies = /* @__PURE__ */ new Set();
|
|
470
|
-
/** 等待变更的 Promise */
|
|
471
|
-
changePromise;
|
|
472
|
-
/** 是否已销毁 */
|
|
473
|
-
destroyed = false;
|
|
474
|
-
/**
|
|
475
|
-
* 追踪依赖
|
|
476
|
-
* 由 ReactiveState.get() 调用
|
|
477
|
-
*/
|
|
478
|
-
track(state) {
|
|
479
|
-
if (!this.destroyed) this.dependencies.add(state);
|
|
480
|
-
}
|
|
481
|
-
/**
|
|
482
|
-
* 通知变更
|
|
483
|
-
* 由 ReactiveState.set() 调用
|
|
484
|
-
*/
|
|
485
|
-
notifyChange() {
|
|
486
|
-
if (!this.destroyed && this.changePromise) this.changePromise.resolve();
|
|
487
|
-
}
|
|
488
|
-
/**
|
|
489
|
-
* 运行响应式任务流
|
|
490
|
-
* 每次依赖变更时重新执行任务并 yield 结果
|
|
491
|
-
*
|
|
492
|
-
* @param task 要执行的异步任务
|
|
493
|
-
* @param signal 用于取消的 AbortSignal
|
|
494
|
-
*/
|
|
495
|
-
async *stream(task, signal) {
|
|
496
|
-
try {
|
|
497
|
-
while (!signal?.aborted && !this.destroyed) {
|
|
498
|
-
this.clearDependencies();
|
|
499
|
-
this.changePromise = createPromiseWithResolvers();
|
|
500
|
-
yield await contextStorage.run(this, task);
|
|
501
|
-
if (this.dependencies.size === 0) break;
|
|
502
|
-
await Promise.race([this.changePromise.promise, signal ? this.waitForAbort(signal) : new Promise(() => {})]);
|
|
503
|
-
if (signal?.aborted) break;
|
|
504
|
-
}
|
|
505
|
-
} finally {
|
|
506
|
-
this.destroy();
|
|
507
|
-
}
|
|
508
|
-
}
|
|
509
|
-
/**
|
|
510
|
-
* 执行一次任务(非响应式)
|
|
511
|
-
* 用于初始数据获取
|
|
512
|
-
*/
|
|
513
|
-
async runOnce(task) {
|
|
514
|
-
return contextStorage.run(this, task);
|
|
515
|
-
}
|
|
516
|
-
/**
|
|
517
|
-
* 清理依赖
|
|
518
|
-
*/
|
|
519
|
-
clearDependencies() {
|
|
520
|
-
for (const state of this.dependencies) state.unsubscribe(this);
|
|
521
|
-
this.dependencies.clear();
|
|
522
|
-
}
|
|
523
|
-
/**
|
|
524
|
-
* 销毁上下文
|
|
525
|
-
* @param reason 可选的销毁原因,如果提供则 reject changePromise
|
|
526
|
-
*/
|
|
527
|
-
destroy(reason) {
|
|
528
|
-
this.destroyed = true;
|
|
529
|
-
this.clearDependencies();
|
|
530
|
-
if (reason && this.changePromise) this.changePromise.reject(reason);
|
|
531
|
-
this.changePromise = void 0;
|
|
532
|
-
}
|
|
533
|
-
/**
|
|
534
|
-
* 等待 AbortSignal
|
|
535
|
-
*/
|
|
536
|
-
waitForAbort(signal) {
|
|
537
|
-
return new Promise((_, reject) => {
|
|
538
|
-
if (signal.aborted) {
|
|
539
|
-
reject(new DOMException("Aborted", "AbortError"));
|
|
540
|
-
return;
|
|
541
|
-
}
|
|
542
|
-
signal.addEventListener("abort", () => {
|
|
543
|
-
reject(new DOMException("Aborted", "AbortError"));
|
|
544
|
-
});
|
|
545
|
-
});
|
|
546
|
-
}
|
|
547
|
-
};
|
|
548
|
-
|
|
549
303
|
//#endregion
|
|
550
304
|
//#region src/reactive-fs/path-realpath.ts
|
|
551
305
|
/**
|
|
@@ -591,6 +345,10 @@ const DEFAULT_IGNORE = [
|
|
|
591
345
|
const RECOVERY_INTERVAL_MS = 3e3;
|
|
592
346
|
/** 路径语义检查间隔 (ms) */
|
|
593
347
|
const PATH_LIVENESS_INTERVAL_MS = 3e3;
|
|
348
|
+
function isStrictAncestorPath(ancestorPath, candidatePath) {
|
|
349
|
+
const childPath = relative(ancestorPath, candidatePath);
|
|
350
|
+
return childPath !== "" && childPath !== ".." && !isAbsolute(childPath) && !childPath.startsWith(`..${sep}`);
|
|
351
|
+
}
|
|
594
352
|
/**
|
|
595
353
|
* 项目监听器
|
|
596
354
|
*
|
|
@@ -780,6 +538,8 @@ var ProjectWatcher = class {
|
|
|
780
538
|
sub.callback(matchedEvents);
|
|
781
539
|
} catch (err) {
|
|
782
540
|
console.error(`[ProjectWatcher] Callback error for ${sub.path}:`, err);
|
|
541
|
+
} finally {
|
|
542
|
+
sub.awaitingPathCreation = sub.watchAncestorsWhileMissing && !existsSync(sub.path);
|
|
783
543
|
}
|
|
784
544
|
}
|
|
785
545
|
}
|
|
@@ -788,6 +548,7 @@ var ProjectWatcher = class {
|
|
|
788
548
|
*/
|
|
789
549
|
matchPath(event, sub) {
|
|
790
550
|
const eventPath = event.path;
|
|
551
|
+
if (sub.watchAncestorsWhileMissing && sub.awaitingPathCreation && isStrictAncestorPath(eventPath, sub.path)) return true;
|
|
791
552
|
if (sub.watchChildren) return eventPath.startsWith(sub.path + "/") || eventPath === sub.path;
|
|
792
553
|
else {
|
|
793
554
|
const eventDir = dirname$1(eventPath);
|
|
@@ -812,6 +573,8 @@ var ProjectWatcher = class {
|
|
|
812
573
|
this.pathSubscriptions.set(id, {
|
|
813
574
|
path: normalizedPath,
|
|
814
575
|
watchChildren: options.watchChildren ?? false,
|
|
576
|
+
watchAncestorsWhileMissing: options.watchAncestorsWhileMissing ?? false,
|
|
577
|
+
awaitingPathCreation: (options.watchAncestorsWhileMissing ?? false) && !existsSync(normalizedPath),
|
|
815
578
|
callback
|
|
816
579
|
});
|
|
817
580
|
return () => {
|
|
@@ -1002,177 +765,265 @@ async function closeAllProjectWatchers() {
|
|
|
1002
765
|
//#endregion
|
|
1003
766
|
//#region src/reactive-fs/watcher-pool.ts
|
|
1004
767
|
/**
|
|
1005
|
-
*
|
|
768
|
+
* Orthogonal intents (updated 2026-07-28 Asia/Shanghai):
|
|
769
|
+
* 1. Own a reference-counted dynamic set of physical watcher roots, including missing logical roots
|
|
770
|
+
* through their nearest existing ancestor.
|
|
771
|
+
* 2. Bind shared path subscriptions to the deepest active containing root.
|
|
772
|
+
* 3. Rebind pending subscriptions when roots appear, disappear, or recover.
|
|
773
|
+
* 4. Expose aggregate and per-root runtime status for server diagnostics.
|
|
774
|
+
* 5. Keep missing-path ancestor settlement explicit in subscription identity and release all leases deterministically.
|
|
775
|
+
*
|
|
776
|
+
* Original request (2026-07-15): "响应式内核要观察 data home、Store roots 和 connected project roots。"
|
|
777
|
+
* Remote CI fixed point (2026-07-28): data-home Schema creation may arrive as an ancestor event on Linux.
|
|
1006
778
|
*/
|
|
779
|
+
const DEBOUNCE_MS = 100;
|
|
1007
780
|
function getRealPath(path) {
|
|
1008
781
|
return resolveRealPathThroughExistingAncestor(path);
|
|
1009
782
|
}
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
783
|
+
function getExistingWatcherRoot(rootPath) {
|
|
784
|
+
let currentPath = rootPath;
|
|
785
|
+
while (!existsSync(currentPath)) {
|
|
786
|
+
const parentPath = dirname$1(currentPath);
|
|
787
|
+
if (parentPath === currentPath) return currentPath;
|
|
788
|
+
currentPath = parentPath;
|
|
789
|
+
}
|
|
790
|
+
return currentPath;
|
|
791
|
+
}
|
|
792
|
+
function pathBelongsToRoot(path, rootPath) {
|
|
793
|
+
const relativePath = relative(rootPath, path);
|
|
794
|
+
return relativePath === "" || !relativePath.startsWith("..") && !isAbsolute(relativePath);
|
|
795
|
+
}
|
|
796
|
+
const watcherRoots = /* @__PURE__ */ new Map();
|
|
797
|
+
const closingWatcherRoots = /* @__PURE__ */ new Map();
|
|
798
|
+
const physicalWatcherReferences = /* @__PURE__ */ new Map();
|
|
799
|
+
const closingPhysicalWatchers = /* @__PURE__ */ new Map();
|
|
1019
800
|
const subscriptionCache = /* @__PURE__ */ new Map();
|
|
1020
|
-
/** 防抖定时器 */
|
|
1021
801
|
const debounceTimers = /* @__PURE__ */ new Map();
|
|
1022
802
|
const watcherRuntimeStatusListeners = /* @__PURE__ */ new Set();
|
|
1023
|
-
let releaseProjectWatcherRuntimeSubscription = null;
|
|
1024
803
|
function emitWatcherRuntimeStatus() {
|
|
1025
804
|
const status = getWatcherRuntimeStatus();
|
|
1026
805
|
for (const listener of watcherRuntimeStatusListeners) listener(status);
|
|
1027
806
|
}
|
|
1028
|
-
function
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
807
|
+
function findWatcherRoot(path) {
|
|
808
|
+
let selected = null;
|
|
809
|
+
for (const root of watcherRoots.values()) if (root.referenceCount > 0 && root.watcher.isInitialized && pathBelongsToRoot(path, root.rootPath) && (!selected || root.rootPath.length > selected.rootPath.length)) selected = root;
|
|
810
|
+
return selected;
|
|
811
|
+
}
|
|
812
|
+
function dispatchSubscription(cacheKey, subscription) {
|
|
813
|
+
const existingTimer = debounceTimers.get(cacheKey);
|
|
814
|
+
if (existingTimer) clearTimeout(existingTimer);
|
|
815
|
+
const timer = setTimeout(() => {
|
|
816
|
+
debounceTimers.delete(cacheKey);
|
|
817
|
+
const current = subscriptionCache.get(cacheKey);
|
|
818
|
+
if (current !== subscription) return;
|
|
819
|
+
for (const callback of current.callbacks) try {
|
|
820
|
+
callback();
|
|
821
|
+
} catch (error) {
|
|
822
|
+
console.error(`[watcher-pool] Callback error for ${subscription.path}:`, error);
|
|
823
|
+
}
|
|
824
|
+
}, subscription.debounceMs);
|
|
825
|
+
debounceTimers.set(cacheKey, timer);
|
|
826
|
+
}
|
|
827
|
+
function rebindSubscription(cacheKey, subscription) {
|
|
828
|
+
const root = findWatcherRoot(subscription.path);
|
|
829
|
+
if (subscription.rootPath === root?.rootPath) return;
|
|
830
|
+
subscription.unsubscribe?.();
|
|
831
|
+
subscription.unsubscribe = null;
|
|
832
|
+
subscription.rootPath = null;
|
|
833
|
+
if (!root) return;
|
|
834
|
+
try {
|
|
835
|
+
subscription.unsubscribe = root.watcher.subscribeSync(subscription.path, () => dispatchSubscription(cacheKey, subscription), {
|
|
836
|
+
watchChildren: subscription.recursive,
|
|
837
|
+
watchAncestorsWhileMissing: subscription.watchAncestorsWhileMissing
|
|
838
|
+
});
|
|
839
|
+
subscription.rootPath = root.rootPath;
|
|
840
|
+
} catch (error) {
|
|
841
|
+
subscription.onError?.();
|
|
842
|
+
console.error(`[watcher-pool] Failed to bind ${subscription.path}:`, error);
|
|
1034
843
|
}
|
|
844
|
+
}
|
|
845
|
+
function rebindAllSubscriptions() {
|
|
846
|
+
for (const [cacheKey, subscription] of subscriptionCache) rebindSubscription(cacheKey, subscription);
|
|
847
|
+
}
|
|
848
|
+
function createWatcherRoot(rootPath) {
|
|
849
|
+
const physicalRootPath = getExistingWatcherRoot(rootPath);
|
|
850
|
+
const existingPhysical = physicalWatcherReferences.get(physicalRootPath);
|
|
851
|
+
const watcher = existingPhysical?.watcher ?? getProjectWatcher(physicalRootPath);
|
|
852
|
+
if (existingPhysical) existingPhysical.referenceCount += 1;
|
|
853
|
+
else physicalWatcherReferences.set(physicalRootPath, {
|
|
854
|
+
watcher,
|
|
855
|
+
referenceCount: 1
|
|
856
|
+
});
|
|
857
|
+
const record = {
|
|
858
|
+
rootPath,
|
|
859
|
+
physicalRootPath,
|
|
860
|
+
watcher,
|
|
861
|
+
referenceCount: 0,
|
|
862
|
+
ready: Promise.resolve(),
|
|
863
|
+
releaseRuntimeStatus: () => {}
|
|
864
|
+
};
|
|
865
|
+
watcherRoots.set(rootPath, record);
|
|
1035
866
|
const forward = () => {
|
|
867
|
+
rebindAllSubscriptions();
|
|
1036
868
|
emitWatcherRuntimeStatus();
|
|
1037
869
|
};
|
|
1038
|
-
|
|
870
|
+
record.releaseRuntimeStatus = watcher.subscribeRuntimeStatus(forward, { emitCurrent: false });
|
|
871
|
+
record.ready = watcher.init().then(() => {
|
|
872
|
+
rebindAllSubscriptions();
|
|
873
|
+
emitWatcherRuntimeStatus();
|
|
874
|
+
});
|
|
875
|
+
return record;
|
|
876
|
+
}
|
|
877
|
+
async function releaseWatcherRootReference(record) {
|
|
878
|
+
if (watcherRoots.get(record.rootPath) !== record || record.referenceCount === 0) return;
|
|
879
|
+
record.referenceCount -= 1;
|
|
880
|
+
if (record.referenceCount > 0) {
|
|
881
|
+
emitWatcherRuntimeStatus();
|
|
882
|
+
return;
|
|
883
|
+
}
|
|
884
|
+
watcherRoots.delete(record.rootPath);
|
|
885
|
+
record.releaseRuntimeStatus();
|
|
886
|
+
rebindAllSubscriptions();
|
|
887
|
+
const physical = physicalWatcherReferences.get(record.physicalRootPath);
|
|
888
|
+
if (!physical || physical.referenceCount <= 0) {
|
|
889
|
+
emitWatcherRuntimeStatus();
|
|
890
|
+
return;
|
|
891
|
+
}
|
|
892
|
+
physical.referenceCount -= 1;
|
|
893
|
+
if (physical.referenceCount > 0) {
|
|
894
|
+
emitWatcherRuntimeStatus();
|
|
895
|
+
return;
|
|
896
|
+
}
|
|
897
|
+
physicalWatcherReferences.delete(record.physicalRootPath);
|
|
898
|
+
const closing = record.watcher.close().finally(() => {
|
|
899
|
+
if (closingWatcherRoots.get(record.rootPath) === closing) closingWatcherRoots.delete(record.rootPath);
|
|
900
|
+
if (closingPhysicalWatchers.get(record.physicalRootPath) === closing) closingPhysicalWatchers.delete(record.physicalRootPath);
|
|
901
|
+
emitWatcherRuntimeStatus();
|
|
902
|
+
});
|
|
903
|
+
closingWatcherRoots.set(record.rootPath, closing);
|
|
904
|
+
closingPhysicalWatchers.set(record.physicalRootPath, closing);
|
|
1039
905
|
emitWatcherRuntimeStatus();
|
|
906
|
+
await closing;
|
|
1040
907
|
}
|
|
1041
|
-
/**
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
908
|
+
/** Acquire one physical observation root and release it when the returned lease is no longer used. */
|
|
909
|
+
async function acquireWatcherRoot(rootPath) {
|
|
910
|
+
const normalizedRoot = getRealPath(rootPath);
|
|
911
|
+
const physicalRootPath = getExistingWatcherRoot(normalizedRoot);
|
|
912
|
+
await Promise.all([closingWatcherRoots.get(normalizedRoot), closingPhysicalWatchers.get(physicalRootPath)]);
|
|
913
|
+
const record = watcherRoots.get(normalizedRoot) ?? createWatcherRoot(normalizedRoot);
|
|
914
|
+
record.referenceCount += 1;
|
|
915
|
+
emitWatcherRuntimeStatus();
|
|
916
|
+
try {
|
|
917
|
+
await record.ready;
|
|
918
|
+
} catch (error) {
|
|
919
|
+
await releaseWatcherRootReference(record);
|
|
920
|
+
throw error;
|
|
921
|
+
}
|
|
922
|
+
let released = false;
|
|
923
|
+
return async () => {
|
|
924
|
+
if (released) return;
|
|
925
|
+
released = true;
|
|
926
|
+
await releaseWatcherRootReference(record);
|
|
927
|
+
};
|
|
1061
928
|
}
|
|
1062
929
|
/**
|
|
1063
|
-
*
|
|
930
|
+
* Acquire a shared path subscription.
|
|
1064
931
|
*
|
|
1065
|
-
*
|
|
1066
|
-
*
|
|
1067
|
-
*
|
|
1068
|
-
* - 同一路径共享订阅
|
|
1069
|
-
* - 引用计数管理生命周期
|
|
1070
|
-
* - 内置防抖机制
|
|
1071
|
-
*
|
|
1072
|
-
* @param path 要监听的路径
|
|
1073
|
-
* @param onChange 变更回调
|
|
1074
|
-
* @param options 监听选项
|
|
1075
|
-
* @returns 释放函数,调用后取消订阅
|
|
932
|
+
* The subscription remains pending when no active root contains the path and binds automatically
|
|
933
|
+
* after a matching root is acquired. This keeps static reads inert without making later live reads
|
|
934
|
+
* permanently lose observation.
|
|
1076
935
|
*/
|
|
1077
936
|
function acquireWatcher(path, onChange, options = {}) {
|
|
1078
|
-
if (!globalProjectWatcher || !globalProjectWatcher.isInitialized) return () => {};
|
|
1079
937
|
const normalizedPath = getRealPath(path);
|
|
1080
|
-
const
|
|
1081
|
-
const
|
|
1082
|
-
const cacheKey = `${normalizedPath}:${
|
|
938
|
+
const recursive = options.recursive ?? false;
|
|
939
|
+
const watchAncestorsWhileMissing = options.watchAncestorsWhileMissing ?? false;
|
|
940
|
+
const cacheKey = `${normalizedPath}:${recursive}:${watchAncestorsWhileMissing}`;
|
|
1083
941
|
let subscription = subscriptionCache.get(cacheKey);
|
|
1084
942
|
if (!subscription) {
|
|
1085
|
-
const unsubscribe = globalProjectWatcher.subscribeSync(normalizedPath, () => {
|
|
1086
|
-
const existingTimer = debounceTimers.get(cacheKey);
|
|
1087
|
-
if (existingTimer) clearTimeout(existingTimer);
|
|
1088
|
-
const timer = setTimeout(() => {
|
|
1089
|
-
debounceTimers.delete(cacheKey);
|
|
1090
|
-
const currentSub = subscriptionCache.get(cacheKey);
|
|
1091
|
-
if (currentSub) for (const cb of currentSub.callbacks) try {
|
|
1092
|
-
cb();
|
|
1093
|
-
} catch (err) {
|
|
1094
|
-
console.error(`[watcher-pool] Callback error for ${normalizedPath}:`, err);
|
|
1095
|
-
}
|
|
1096
|
-
}, debounceMs);
|
|
1097
|
-
debounceTimers.set(cacheKey, timer);
|
|
1098
|
-
}, { watchChildren: isRecursive });
|
|
1099
943
|
subscription = {
|
|
1100
944
|
path: normalizedPath,
|
|
945
|
+
recursive,
|
|
946
|
+
watchAncestorsWhileMissing,
|
|
947
|
+
debounceMs: options.debounceMs ?? DEBOUNCE_MS,
|
|
1101
948
|
callbacks: /* @__PURE__ */ new Set(),
|
|
1102
|
-
|
|
949
|
+
rootPath: null,
|
|
950
|
+
unsubscribe: null,
|
|
1103
951
|
onError: options.onError
|
|
1104
952
|
};
|
|
1105
953
|
subscriptionCache.set(cacheKey, subscription);
|
|
954
|
+
rebindSubscription(cacheKey, subscription);
|
|
1106
955
|
}
|
|
1107
956
|
subscription.callbacks.add(onChange);
|
|
957
|
+
let released = false;
|
|
1108
958
|
return () => {
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
if (
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
959
|
+
if (released) return;
|
|
960
|
+
released = true;
|
|
961
|
+
const current = subscriptionCache.get(cacheKey);
|
|
962
|
+
if (!current) return;
|
|
963
|
+
current.callbacks.delete(onChange);
|
|
964
|
+
if (current.callbacks.size > 0) return;
|
|
965
|
+
current.unsubscribe?.();
|
|
966
|
+
subscriptionCache.delete(cacheKey);
|
|
967
|
+
const timer = debounceTimers.get(cacheKey);
|
|
968
|
+
if (timer) {
|
|
969
|
+
clearTimeout(timer);
|
|
970
|
+
debounceTimers.delete(cacheKey);
|
|
1120
971
|
}
|
|
1121
972
|
};
|
|
1122
973
|
}
|
|
1123
|
-
/**
|
|
1124
|
-
* 获取当前活跃的监听器数量(用于调试)
|
|
1125
|
-
*/
|
|
1126
974
|
function getActiveWatcherCount() {
|
|
1127
975
|
return subscriptionCache.size;
|
|
1128
976
|
}
|
|
1129
|
-
/**
|
|
1130
|
-
* 关闭所有监听器(用于测试清理)
|
|
1131
|
-
*/
|
|
977
|
+
/** Close the entire process-level pool. Intended for runtime teardown and test isolation. */
|
|
1132
978
|
async function closeAllWatchers() {
|
|
1133
|
-
for (const [
|
|
1134
|
-
|
|
1135
|
-
const timer = debounceTimers.get(
|
|
979
|
+
for (const [cacheKey, subscription] of subscriptionCache) {
|
|
980
|
+
subscription.unsubscribe?.();
|
|
981
|
+
const timer = debounceTimers.get(cacheKey);
|
|
1136
982
|
if (timer) clearTimeout(timer);
|
|
1137
983
|
}
|
|
1138
984
|
subscriptionCache.clear();
|
|
1139
985
|
debounceTimers.clear();
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
}
|
|
986
|
+
const records = [...watcherRoots.values()];
|
|
987
|
+
watcherRoots.clear();
|
|
988
|
+
for (const record of records) {
|
|
989
|
+
record.referenceCount = 0;
|
|
990
|
+
record.releaseRuntimeStatus();
|
|
991
|
+
}
|
|
992
|
+
const physicalWatchers = [...physicalWatcherReferences.values()].map(({ watcher }) => watcher);
|
|
993
|
+
physicalWatcherReferences.clear();
|
|
994
|
+
await Promise.all([
|
|
995
|
+
...physicalWatchers.map((watcher) => watcher.close()),
|
|
996
|
+
...closingWatcherRoots.values(),
|
|
997
|
+
...closingPhysicalWatchers.values()
|
|
998
|
+
]);
|
|
999
|
+
closingWatcherRoots.clear();
|
|
1000
|
+
closingPhysicalWatchers.clear();
|
|
1147
1001
|
emitWatcherRuntimeStatus();
|
|
1148
1002
|
}
|
|
1149
|
-
/**
|
|
1150
|
-
* 检查 watcher pool 是否已初始化
|
|
1151
|
-
*/
|
|
1152
1003
|
function isWatcherPoolInitialized() {
|
|
1153
|
-
return
|
|
1154
|
-
}
|
|
1155
|
-
/**
|
|
1156
|
-
* 获取当前监听的项目目录
|
|
1157
|
-
*/
|
|
1158
|
-
function getWatchedProjectDir() {
|
|
1159
|
-
return globalProjectDir;
|
|
1004
|
+
return [...watcherRoots.values()].some((root) => root.watcher.isInitialized);
|
|
1160
1005
|
}
|
|
1161
|
-
/**
|
|
1162
|
-
* 获取 watcher 运行时状态
|
|
1163
|
-
*/
|
|
1164
1006
|
function getWatcherRuntimeStatus() {
|
|
1165
|
-
|
|
1166
|
-
|
|
1007
|
+
const roots = [...watcherRoots.values()].filter((root) => root.referenceCount > 0).map((root) => {
|
|
1008
|
+
const runtime = root.watcher.runtimeStatus;
|
|
1009
|
+
return {
|
|
1010
|
+
rootPath: root.rootPath,
|
|
1011
|
+
referenceCount: root.referenceCount,
|
|
1012
|
+
initialized: root.watcher.isInitialized,
|
|
1013
|
+
subscriptionCount: root.watcher.subscriptionCount,
|
|
1014
|
+
generation: runtime.generation,
|
|
1015
|
+
reinitializeCount: runtime.reinitializeCount,
|
|
1016
|
+
lastReinitializeReason: runtime.lastReinitializeReason,
|
|
1017
|
+
reinitializeReasonCounts: runtime.reinitializeReasonCounts,
|
|
1018
|
+
projectResidency: runtime.projectResidency
|
|
1019
|
+
};
|
|
1020
|
+
}).sort((left, right) => left.rootPath.localeCompare(right.rootPath));
|
|
1021
|
+
if (roots.length === 0) return null;
|
|
1167
1022
|
return {
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
subscriptionCount:
|
|
1171
|
-
|
|
1172
|
-
reinitializeCount: runtime.reinitializeCount,
|
|
1173
|
-
lastReinitializeReason: runtime.lastReinitializeReason,
|
|
1174
|
-
reinitializeReasonCounts: runtime.reinitializeReasonCounts,
|
|
1175
|
-
projectResidency: runtime.projectResidency
|
|
1023
|
+
initialized: roots.some((root) => root.initialized),
|
|
1024
|
+
rootCount: roots.length,
|
|
1025
|
+
subscriptionCount: roots.reduce((count, root) => count + root.subscriptionCount, 0),
|
|
1026
|
+
roots
|
|
1176
1027
|
};
|
|
1177
1028
|
}
|
|
1178
1029
|
function subscribeWatcherRuntimeStatus(listener, options = {}) {
|
|
@@ -1184,26 +1035,301 @@ function subscribeWatcherRuntimeStatus(listener, options = {}) {
|
|
|
1184
1035
|
}
|
|
1185
1036
|
|
|
1186
1037
|
//#endregion
|
|
1187
|
-
//#region src/reactive-fs/
|
|
1188
|
-
/**
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1038
|
+
//#region src/reactive-fs/observation-environment.ts
|
|
1039
|
+
/**
|
|
1040
|
+
* Orthogonal intents (created 2026-07-16 Asia/Shanghai):
|
|
1041
|
+
* 1. Own the dynamic observation-root set for one backend runtime environment.
|
|
1042
|
+
* 2. Reference-count repeated logical acquisition inside that environment.
|
|
1043
|
+
* 3. Share physical root watchers through the process-level watcher pool.
|
|
1044
|
+
* 4. Release every root deterministically during environment teardown.
|
|
1045
|
+
*
|
|
1046
|
+
* Original request (2026-07-15): "Reactive observation supports a reference-counted dynamic set of roots per runtime environment."
|
|
1047
|
+
*/
|
|
1048
|
+
/** Runtime-environment owner for all roots observed by one backend process. */
|
|
1049
|
+
var ReactiveObservationEnvironment = class {
|
|
1050
|
+
roots = /* @__PURE__ */ new Map();
|
|
1051
|
+
disposed = false;
|
|
1052
|
+
async acquireRoot(rootPath) {
|
|
1053
|
+
if (this.disposed) throw new Error("Cannot acquire an observation root after environment teardown.");
|
|
1054
|
+
const normalizedRoot = resolveRealPathThroughExistingAncestor(rootPath);
|
|
1055
|
+
let record = this.roots.get(normalizedRoot);
|
|
1056
|
+
if (!record) {
|
|
1057
|
+
record = {
|
|
1058
|
+
rootPath: normalizedRoot,
|
|
1059
|
+
referenceCount: 0,
|
|
1060
|
+
lease: acquireWatcherRoot(normalizedRoot)
|
|
1061
|
+
};
|
|
1062
|
+
this.roots.set(normalizedRoot, record);
|
|
1063
|
+
}
|
|
1064
|
+
record.referenceCount += 1;
|
|
1065
|
+
try {
|
|
1066
|
+
await record.lease;
|
|
1067
|
+
} catch (error) {
|
|
1068
|
+
record.referenceCount -= 1;
|
|
1069
|
+
if (record.referenceCount === 0 && this.roots.get(normalizedRoot) === record) this.roots.delete(normalizedRoot);
|
|
1070
|
+
throw error;
|
|
1071
|
+
}
|
|
1072
|
+
let released = false;
|
|
1073
|
+
return async () => {
|
|
1074
|
+
if (released) return;
|
|
1075
|
+
released = true;
|
|
1076
|
+
const current = this.roots.get(normalizedRoot);
|
|
1077
|
+
if (current !== record || current.referenceCount === 0) return;
|
|
1078
|
+
current.referenceCount -= 1;
|
|
1079
|
+
if (current.referenceCount > 0) return;
|
|
1080
|
+
this.roots.delete(normalizedRoot);
|
|
1081
|
+
await (await current.lease)();
|
|
1082
|
+
};
|
|
1083
|
+
}
|
|
1084
|
+
getRoots() {
|
|
1085
|
+
return [...this.roots.values()].map(({ rootPath, referenceCount }) => ({
|
|
1086
|
+
rootPath,
|
|
1087
|
+
referenceCount
|
|
1088
|
+
})).sort((left, right) => left.rootPath.localeCompare(right.rootPath));
|
|
1089
|
+
}
|
|
1090
|
+
async dispose() {
|
|
1091
|
+
if (this.disposed) return;
|
|
1092
|
+
this.disposed = true;
|
|
1093
|
+
const records = [...this.roots.values()];
|
|
1094
|
+
this.roots.clear();
|
|
1095
|
+
await Promise.all(records.map(async (record) => {
|
|
1096
|
+
await (await record.lease)();
|
|
1097
|
+
}));
|
|
1098
|
+
}
|
|
1099
|
+
};
|
|
1100
|
+
|
|
1101
|
+
//#endregion
|
|
1102
|
+
//#region src/reactive-fs/reactive-state.ts
|
|
1103
|
+
/**
|
|
1104
|
+
* 全局的 AsyncLocalStorage,用于在异步调用链中传递 ReactiveContext
|
|
1105
|
+
* 这是实现依赖收集的核心机制
|
|
1106
|
+
*/
|
|
1107
|
+
const contextStorage = new AsyncLocalStorage();
|
|
1108
|
+
/**
|
|
1109
|
+
* 响应式状态类,类似 Signal.State
|
|
1110
|
+
*
|
|
1111
|
+
* 核心机制:
|
|
1112
|
+
* - get() 时自动注册到当前 ReactiveContext 的依赖列表
|
|
1113
|
+
* - set() 时如果值变化,通知所有依赖的 Context
|
|
1114
|
+
*/
|
|
1115
|
+
var ReactiveState = class {
|
|
1116
|
+
currentValue;
|
|
1117
|
+
equals;
|
|
1118
|
+
/** 所有依赖此状态的 Context */
|
|
1119
|
+
subscribers = /* @__PURE__ */ new Set();
|
|
1120
|
+
constructor(initialValue, options) {
|
|
1121
|
+
this.currentValue = initialValue;
|
|
1122
|
+
this.equals = options?.equals ?? ((a, b) => a === b);
|
|
1123
|
+
}
|
|
1124
|
+
/**
|
|
1125
|
+
* 获取当前值
|
|
1126
|
+
* 如果在 ReactiveContext 中调用,会自动注册依赖
|
|
1127
|
+
*/
|
|
1128
|
+
get() {
|
|
1129
|
+
const context = contextStorage.getStore();
|
|
1130
|
+
if (context) {
|
|
1131
|
+
context.track(this);
|
|
1132
|
+
this.subscribers.add(context);
|
|
1133
|
+
}
|
|
1134
|
+
return this.currentValue;
|
|
1135
|
+
}
|
|
1136
|
+
/**
|
|
1137
|
+
* 设置新值
|
|
1138
|
+
* 如果值变化,通知所有订阅者
|
|
1139
|
+
* @returns 是否发生了变化
|
|
1140
|
+
*/
|
|
1141
|
+
set(newValue) {
|
|
1142
|
+
if (this.equals(this.currentValue, newValue)) return false;
|
|
1143
|
+
this.currentValue = newValue;
|
|
1144
|
+
for (const context of this.subscribers) context.notifyChange();
|
|
1145
|
+
return true;
|
|
1146
|
+
}
|
|
1147
|
+
/**
|
|
1148
|
+
* 取消订阅
|
|
1149
|
+
* 当 Context 销毁时调用
|
|
1150
|
+
*/
|
|
1151
|
+
unsubscribe(context) {
|
|
1152
|
+
this.subscribers.delete(context);
|
|
1153
|
+
}
|
|
1154
|
+
/**
|
|
1155
|
+
* 获取当前订阅者数量(用于调试)
|
|
1156
|
+
*/
|
|
1157
|
+
get subscriberCount() {
|
|
1158
|
+
return this.subscribers.size;
|
|
1159
|
+
}
|
|
1160
|
+
};
|
|
1161
|
+
|
|
1162
|
+
//#endregion
|
|
1163
|
+
//#region src/reactive-fs/reactive-context.ts
|
|
1164
|
+
/**
|
|
1165
|
+
* Orthogonal intents (updated 2026-07-26 Asia/Shanghai):
|
|
1166
|
+
* 1. Collect reactive dependencies for asynchronous projections.
|
|
1167
|
+
* 2. Coalesce dependency changes into replacement task executions.
|
|
1168
|
+
* 3. Expose opt-in recompute lifecycle without changing yielded data.
|
|
1169
|
+
*
|
|
1170
|
+
* Original request (2026-07-22): "整个过程中,几乎都在 Loading。"
|
|
1171
|
+
* Original request (2026-07-26): "真正基于文件、甚至是文件内容结构的变更去拉取更新。"
|
|
1172
|
+
*/
|
|
1173
|
+
function createPromiseWithResolvers() {
|
|
1174
|
+
let resolve$1;
|
|
1175
|
+
let reject;
|
|
1176
|
+
return {
|
|
1177
|
+
promise: new Promise((res, rej) => {
|
|
1178
|
+
resolve$1 = res;
|
|
1179
|
+
reject = rej;
|
|
1180
|
+
}),
|
|
1181
|
+
resolve: resolve$1,
|
|
1182
|
+
reject
|
|
1183
|
+
};
|
|
1184
|
+
}
|
|
1185
|
+
/**
|
|
1186
|
+
* 响应式上下文,管理依赖收集和变更通知
|
|
1187
|
+
*
|
|
1188
|
+
* 核心机制:
|
|
1189
|
+
* - 在 stream() 中执行任务时,通过 AsyncLocalStorage 传递 this
|
|
1190
|
+
* - 任务中的所有 ReactiveState.get() 调用都会自动注册依赖
|
|
1191
|
+
* - 当任何依赖变更时,重新执行任务并 yield 新结果
|
|
1192
|
+
*/
|
|
1193
|
+
var ReactiveContext = class {
|
|
1194
|
+
/** 当前追踪的依赖 */
|
|
1195
|
+
dependencies = /* @__PURE__ */ new Set();
|
|
1196
|
+
/** 等待变更的 Promise */
|
|
1197
|
+
changePromise;
|
|
1198
|
+
/** 是否已销毁 */
|
|
1199
|
+
destroyed = false;
|
|
1200
|
+
/** 当前任务执行期间是否已有依赖变化,使其结果不再可提交 */
|
|
1201
|
+
changePending = false;
|
|
1202
|
+
/**
|
|
1203
|
+
* 追踪依赖
|
|
1204
|
+
* 由 ReactiveState.get() 调用
|
|
1205
|
+
*/
|
|
1206
|
+
track(state) {
|
|
1207
|
+
if (!this.destroyed) this.dependencies.add(state);
|
|
1208
|
+
}
|
|
1209
|
+
/**
|
|
1210
|
+
* 通知变更
|
|
1211
|
+
* 由 ReactiveState.set() 调用
|
|
1212
|
+
*/
|
|
1213
|
+
notifyChange() {
|
|
1214
|
+
if (this.destroyed) return;
|
|
1215
|
+
this.changePending = true;
|
|
1216
|
+
this.changePromise?.resolve();
|
|
1217
|
+
}
|
|
1218
|
+
/**
|
|
1219
|
+
* 运行响应式任务流
|
|
1220
|
+
* 每次依赖变更时重新执行任务并 yield 结果
|
|
1221
|
+
*
|
|
1222
|
+
* @param task 要执行的异步任务
|
|
1223
|
+
* @param signal 用于取消的 AbortSignal
|
|
1224
|
+
* @param observer 可选的依赖重算生命周期观察器
|
|
1225
|
+
*/
|
|
1226
|
+
async *stream(task, signal, observer) {
|
|
1227
|
+
let abortPromise = null;
|
|
1228
|
+
try {
|
|
1229
|
+
while (!signal?.aborted && !this.destroyed) {
|
|
1230
|
+
this.clearDependencies();
|
|
1231
|
+
this.changePending = false;
|
|
1232
|
+
this.changePromise = createPromiseWithResolvers();
|
|
1233
|
+
let result;
|
|
1234
|
+
try {
|
|
1235
|
+
result = await contextStorage.run(this, task);
|
|
1236
|
+
} catch (error) {
|
|
1237
|
+
if (this.changePending) {
|
|
1238
|
+
if (signal?.aborted) break;
|
|
1239
|
+
observer?.onRecomputeStarted();
|
|
1240
|
+
continue;
|
|
1241
|
+
}
|
|
1242
|
+
throw error;
|
|
1243
|
+
}
|
|
1244
|
+
if (signal?.aborted) break;
|
|
1245
|
+
if (this.changePending) {
|
|
1246
|
+
if (signal?.aborted) break;
|
|
1247
|
+
observer?.onRecomputeStarted();
|
|
1248
|
+
continue;
|
|
1249
|
+
}
|
|
1250
|
+
yield result;
|
|
1251
|
+
if (this.dependencies.size === 0) break;
|
|
1252
|
+
abortPromise ??= signal ? this.waitForAbort(signal) : new Promise(() => {});
|
|
1253
|
+
await Promise.race([this.changePromise.promise, abortPromise]);
|
|
1254
|
+
if (signal?.aborted) break;
|
|
1255
|
+
observer?.onRecomputeStarted();
|
|
1256
|
+
}
|
|
1257
|
+
} finally {
|
|
1258
|
+
this.destroy();
|
|
1259
|
+
}
|
|
1260
|
+
}
|
|
1261
|
+
/**
|
|
1262
|
+
* 执行一次任务(非响应式)
|
|
1263
|
+
* 用于初始数据获取
|
|
1264
|
+
*/
|
|
1265
|
+
async runOnce(task) {
|
|
1266
|
+
return contextStorage.run(this, task);
|
|
1267
|
+
}
|
|
1268
|
+
/**
|
|
1269
|
+
* 清理依赖
|
|
1270
|
+
*/
|
|
1271
|
+
clearDependencies() {
|
|
1272
|
+
for (const state of this.dependencies) state.unsubscribe(this);
|
|
1273
|
+
this.dependencies.clear();
|
|
1274
|
+
}
|
|
1275
|
+
/**
|
|
1276
|
+
* 销毁上下文
|
|
1277
|
+
* @param reason 可选的销毁原因,如果提供则 reject changePromise
|
|
1278
|
+
*/
|
|
1279
|
+
destroy(reason) {
|
|
1280
|
+
this.destroyed = true;
|
|
1281
|
+
this.clearDependencies();
|
|
1282
|
+
if (reason && this.changePromise) this.changePromise.reject(reason);
|
|
1283
|
+
this.changePromise = void 0;
|
|
1284
|
+
}
|
|
1285
|
+
/**
|
|
1286
|
+
* 等待 AbortSignal
|
|
1287
|
+
*/
|
|
1288
|
+
waitForAbort(signal) {
|
|
1289
|
+
return new Promise((_, reject) => {
|
|
1290
|
+
if (signal.aborted) {
|
|
1291
|
+
reject(new DOMException("Aborted", "AbortError"));
|
|
1292
|
+
return;
|
|
1293
|
+
}
|
|
1294
|
+
signal.addEventListener("abort", () => {
|
|
1295
|
+
reject(new DOMException("Aborted", "AbortError"));
|
|
1296
|
+
});
|
|
1297
|
+
});
|
|
1298
|
+
}
|
|
1299
|
+
};
|
|
1300
|
+
|
|
1301
|
+
//#endregion
|
|
1302
|
+
//#region src/reactive-fs/reactive-fs.ts
|
|
1303
|
+
/**
|
|
1304
|
+
* Orthogonal intents (updated 2026-07-16 Asia/Shanghai):
|
|
1305
|
+
* 1. Cache reactive file, directory, existence, and stat reads.
|
|
1306
|
+
* 2. Bind cached reads to shared watcher roots and missing-path fallback checks.
|
|
1307
|
+
* 3. Settle overlapping cached projections as one coherent post-mutation snapshot.
|
|
1308
|
+
* 4. Release watcher, polling, state, and refresh registrations together.
|
|
1309
|
+
*
|
|
1310
|
+
* Original request (2026-07-16): "direct filesystem mutations update reactive state before returning."
|
|
1311
|
+
*/
|
|
1312
|
+
/** 状态缓存:路径 -> ReactiveState */
|
|
1313
|
+
const stateCache$1 = /* @__PURE__ */ new Map();
|
|
1314
|
+
/** Cache refreshers retained so owned writes can settle projections without watcher timing. */
|
|
1315
|
+
const refreshCache = /* @__PURE__ */ new Map();
|
|
1316
|
+
/** 监听器释放函数缓存 */
|
|
1317
|
+
const releaseCache$1 = /* @__PURE__ */ new Map();
|
|
1318
|
+
/** 缺失路径重校验定时器缓存 */
|
|
1319
|
+
const missingPathPollCache = /* @__PURE__ */ new Map();
|
|
1320
|
+
/** Native watcher can miss a create event on some CI filesystems; missing paths get a low-frequency fallback. */
|
|
1321
|
+
const MISSING_PATH_POLL_MS = 1e3;
|
|
1322
|
+
function stopMissingPathPoll(key) {
|
|
1323
|
+
const timer = missingPathPollCache.get(key);
|
|
1324
|
+
if (!timer) return;
|
|
1325
|
+
clearInterval(timer);
|
|
1326
|
+
missingPathPollCache.delete(key);
|
|
1327
|
+
}
|
|
1328
|
+
function ensureMissingPathPoll(key, poll) {
|
|
1329
|
+
if (!isWatcherPoolInitialized() || missingPathPollCache.has(key)) return;
|
|
1330
|
+
const timer = setInterval(() => {
|
|
1331
|
+
poll();
|
|
1332
|
+
}, MISSING_PATH_POLL_MS);
|
|
1207
1333
|
timer.unref?.();
|
|
1208
1334
|
missingPathPollCache.set(key, timer);
|
|
1209
1335
|
}
|
|
@@ -1224,7 +1350,7 @@ async function reactiveReadFile(filepath) {
|
|
|
1224
1350
|
const key = `file:${normalizedPath}`;
|
|
1225
1351
|
const getValue = async () => {
|
|
1226
1352
|
try {
|
|
1227
|
-
return await readFile
|
|
1353
|
+
return await readFile(normalizedPath, "utf-8");
|
|
1228
1354
|
} catch {
|
|
1229
1355
|
return null;
|
|
1230
1356
|
}
|
|
@@ -1234,16 +1360,28 @@ async function reactiveReadFile(filepath) {
|
|
|
1234
1360
|
const initialValue = await getValue();
|
|
1235
1361
|
state = new ReactiveState(initialValue);
|
|
1236
1362
|
stateCache$1.set(key, state);
|
|
1237
|
-
const
|
|
1363
|
+
const prepareRefresh = async () => {
|
|
1238
1364
|
const newValue = await getValue();
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1365
|
+
return () => {
|
|
1366
|
+
state.set(newValue);
|
|
1367
|
+
if (newValue === null) ensureMissingPathPoll(key, refresh);
|
|
1368
|
+
else stopMissingPathPoll(key);
|
|
1369
|
+
};
|
|
1370
|
+
};
|
|
1371
|
+
const refresh = async () => {
|
|
1372
|
+
(await prepareRefresh())();
|
|
1242
1373
|
};
|
|
1374
|
+
refreshCache.set(key, {
|
|
1375
|
+
kind: "file",
|
|
1376
|
+
path: normalizedPath,
|
|
1377
|
+
prepareRefresh,
|
|
1378
|
+
refresh
|
|
1379
|
+
});
|
|
1243
1380
|
if (initialValue === null) ensureMissingPathPoll(key, refresh);
|
|
1244
1381
|
const release = acquireWatcher(dirname$1(normalizedPath), refresh, { onError: () => {
|
|
1245
1382
|
stopMissingPathPoll(key);
|
|
1246
1383
|
stateCache$1.delete(key);
|
|
1384
|
+
refreshCache.delete(key);
|
|
1247
1385
|
releaseCache$1.delete(key);
|
|
1248
1386
|
} });
|
|
1249
1387
|
releaseCache$1.set(key, release);
|
|
@@ -1259,6 +1397,31 @@ function updateReactiveFileCache(filepath, content) {
|
|
|
1259
1397
|
const key = `file:${resolve(filepath)}`;
|
|
1260
1398
|
stateCache$1.get(key)?.set(content);
|
|
1261
1399
|
}
|
|
1400
|
+
function containsPath(rootPath, candidatePath) {
|
|
1401
|
+
const childPath = relative(rootPath, candidatePath);
|
|
1402
|
+
return childPath === "" || !isAbsolute(childPath) && !childPath.startsWith(`..${sep}`) && childPath !== "..";
|
|
1403
|
+
}
|
|
1404
|
+
function pathsOverlap(firstPath, secondPath) {
|
|
1405
|
+
return containsPath(firstPath, secondPath) || containsPath(secondPath, firstPath);
|
|
1406
|
+
}
|
|
1407
|
+
async function settleReactivePathMutationInternal(path, exactFileContent) {
|
|
1408
|
+
const normalizedPath = resolve(path);
|
|
1409
|
+
const registrations = [...refreshCache.values()].filter((registration) => pathsOverlap(registration.path, normalizedPath));
|
|
1410
|
+
const commits = await Promise.all(registrations.flatMap((registration) => exactFileContent !== void 0 && registration.kind === "file" && registration.path === normalizedPath ? [] : [registration.prepareRefresh()]));
|
|
1411
|
+
for (const commit of commits) commit();
|
|
1412
|
+
if (exactFileContent !== void 0) updateReactiveFileCache(normalizedPath, exactFileContent);
|
|
1413
|
+
}
|
|
1414
|
+
/** Re-read and atomically publish every cached projection overlapping one committed path mutation. */
|
|
1415
|
+
async function settleReactivePathMutation(path) {
|
|
1416
|
+
await settleReactivePathMutationInternal(path);
|
|
1417
|
+
}
|
|
1418
|
+
/**
|
|
1419
|
+
* Publish a successful file write to every cached projection whose value can change.
|
|
1420
|
+
* File content is set from the committed bytes; directory/existence/stat views re-read disk.
|
|
1421
|
+
*/
|
|
1422
|
+
async function settleReactiveFileWrite(filepath, content) {
|
|
1423
|
+
await settleReactivePathMutationInternal(filepath, content);
|
|
1424
|
+
}
|
|
1262
1425
|
/**
|
|
1263
1426
|
* 响应式读取目录内容
|
|
1264
1427
|
*
|
|
@@ -1292,16 +1455,29 @@ async function reactiveReadDir(dirpath, options = {}) {
|
|
|
1292
1455
|
if (!state) {
|
|
1293
1456
|
state = new ReactiveState(await getValue(), { equals: (a, b) => a.length === b.length && a.every((v, i) => v === b[i]) });
|
|
1294
1457
|
stateCache$1.set(key, state);
|
|
1295
|
-
const
|
|
1458
|
+
const prepareRefresh = async () => {
|
|
1296
1459
|
const newValue = await getValue();
|
|
1297
|
-
|
|
1298
|
-
|
|
1460
|
+
return () => {
|
|
1461
|
+
state.set(newValue);
|
|
1462
|
+
};
|
|
1463
|
+
};
|
|
1464
|
+
const refresh = async () => {
|
|
1465
|
+
(await prepareRefresh())();
|
|
1466
|
+
};
|
|
1467
|
+
const release = acquireWatcher(normalizedPath, refresh, {
|
|
1299
1468
|
recursive: true,
|
|
1300
1469
|
onError: () => {
|
|
1301
1470
|
stateCache$1.delete(key);
|
|
1471
|
+
refreshCache.delete(key);
|
|
1302
1472
|
releaseCache$1.delete(key);
|
|
1303
1473
|
}
|
|
1304
1474
|
});
|
|
1475
|
+
refreshCache.set(key, {
|
|
1476
|
+
kind: "directory",
|
|
1477
|
+
path: normalizedPath,
|
|
1478
|
+
prepareRefresh,
|
|
1479
|
+
refresh
|
|
1480
|
+
});
|
|
1305
1481
|
releaseCache$1.set(key, release);
|
|
1306
1482
|
}
|
|
1307
1483
|
return state.get();
|
|
@@ -1328,16 +1504,28 @@ async function reactiveExists(path) {
|
|
|
1328
1504
|
const initialValue = await getValue();
|
|
1329
1505
|
state = new ReactiveState(initialValue);
|
|
1330
1506
|
stateCache$1.set(key, state);
|
|
1331
|
-
const
|
|
1507
|
+
const prepareRefresh = async () => {
|
|
1332
1508
|
const newValue = await getValue();
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1509
|
+
return () => {
|
|
1510
|
+
state.set(newValue);
|
|
1511
|
+
if (newValue) stopMissingPathPoll(key);
|
|
1512
|
+
else ensureMissingPathPoll(key, refresh);
|
|
1513
|
+
};
|
|
1514
|
+
};
|
|
1515
|
+
const refresh = async () => {
|
|
1516
|
+
(await prepareRefresh())();
|
|
1336
1517
|
};
|
|
1518
|
+
refreshCache.set(key, {
|
|
1519
|
+
kind: "exists",
|
|
1520
|
+
path: normalizedPath,
|
|
1521
|
+
prepareRefresh,
|
|
1522
|
+
refresh
|
|
1523
|
+
});
|
|
1337
1524
|
if (!initialValue) ensureMissingPathPoll(key, refresh);
|
|
1338
1525
|
const release = acquireWatcher(dirname$1(normalizedPath), refresh, { onError: () => {
|
|
1339
1526
|
stopMissingPathPoll(key);
|
|
1340
1527
|
stateCache$1.delete(key);
|
|
1528
|
+
refreshCache.delete(key);
|
|
1341
1529
|
releaseCache$1.delete(key);
|
|
1342
1530
|
} });
|
|
1343
1531
|
releaseCache$1.set(key, release);
|
|
@@ -1375,16 +1563,28 @@ async function reactiveStat(path) {
|
|
|
1375
1563
|
return a.isDirectory === b.isDirectory && a.isFile === b.isFile && a.mtime === b.mtime && a.birthtime === b.birthtime;
|
|
1376
1564
|
} });
|
|
1377
1565
|
stateCache$1.set(key, state);
|
|
1378
|
-
const
|
|
1566
|
+
const prepareRefresh = async () => {
|
|
1379
1567
|
const newValue = await getValue();
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1568
|
+
return () => {
|
|
1569
|
+
state.set(newValue);
|
|
1570
|
+
if (newValue === null) ensureMissingPathPoll(key, refresh);
|
|
1571
|
+
else stopMissingPathPoll(key);
|
|
1572
|
+
};
|
|
1383
1573
|
};
|
|
1574
|
+
const refresh = async () => {
|
|
1575
|
+
(await prepareRefresh())();
|
|
1576
|
+
};
|
|
1577
|
+
refreshCache.set(key, {
|
|
1578
|
+
kind: "stat",
|
|
1579
|
+
path: normalizedPath,
|
|
1580
|
+
prepareRefresh,
|
|
1581
|
+
refresh
|
|
1582
|
+
});
|
|
1384
1583
|
if (initialValue === null) ensureMissingPathPoll(key, refresh);
|
|
1385
1584
|
const release = acquireWatcher(dirname$1(normalizedPath), refresh, { onError: () => {
|
|
1386
1585
|
stopMissingPathPoll(key);
|
|
1387
1586
|
stateCache$1.delete(key);
|
|
1587
|
+
refreshCache.delete(key);
|
|
1388
1588
|
releaseCache$1.delete(key);
|
|
1389
1589
|
} });
|
|
1390
1590
|
releaseCache$1.set(key, release);
|
|
@@ -1392,30 +1592,154 @@ async function reactiveStat(path) {
|
|
|
1392
1592
|
return state.get();
|
|
1393
1593
|
}
|
|
1394
1594
|
/**
|
|
1395
|
-
* 清除指定路径的缓存(用于测试)
|
|
1595
|
+
* 清除指定路径的缓存(用于测试)
|
|
1596
|
+
*/
|
|
1597
|
+
function clearCache(path) {
|
|
1598
|
+
if (path) {
|
|
1599
|
+
const normalizedPath = resolve(path);
|
|
1600
|
+
for (const [key, release] of releaseCache$1) if (key.includes(normalizedPath)) {
|
|
1601
|
+
release();
|
|
1602
|
+
stopMissingPathPoll(key);
|
|
1603
|
+
releaseCache$1.delete(key);
|
|
1604
|
+
stateCache$1.delete(key);
|
|
1605
|
+
refreshCache.delete(key);
|
|
1606
|
+
}
|
|
1607
|
+
} else {
|
|
1608
|
+
for (const release of releaseCache$1.values()) release();
|
|
1609
|
+
releaseCache$1.clear();
|
|
1610
|
+
for (const timer of missingPathPollCache.values()) clearInterval(timer);
|
|
1611
|
+
missingPathPollCache.clear();
|
|
1612
|
+
stateCache$1.clear();
|
|
1613
|
+
refreshCache.clear();
|
|
1614
|
+
}
|
|
1615
|
+
}
|
|
1616
|
+
/**
|
|
1617
|
+
* 获取缓存大小(用于调试)
|
|
1618
|
+
*/
|
|
1619
|
+
function getCacheSize() {
|
|
1620
|
+
return stateCache$1.size;
|
|
1621
|
+
}
|
|
1622
|
+
|
|
1623
|
+
//#endregion
|
|
1624
|
+
//#region src/physical-reactive-file-writer.ts
|
|
1625
|
+
/**
|
|
1626
|
+
* Orthogonal intents (updated 2026-07-16 Asia/Shanghai):
|
|
1627
|
+
* 1. Own lexical and existing-ancestor physical confinement for root-scoped mutations.
|
|
1628
|
+
* 2. Execute UTF-8 write, directory-create, remove, and guarded external mutations.
|
|
1629
|
+
* 3. Settle overlapping reactive projections before returning any terminal outcome.
|
|
1630
|
+
* 4. Keep TOCTOU and hard-link alias limitations explicit at the mutation seam.
|
|
1631
|
+
*
|
|
1632
|
+
* Original request (2026-07-16): "建立唯一的 physical/reactive entity-write owner。"
|
|
1633
|
+
* Original request (2026-07-16): "Schema/Template mutations must reject symlink escape and settle reactive projections before success."
|
|
1634
|
+
*/
|
|
1635
|
+
function isMissingPath(error) {
|
|
1636
|
+
return typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT";
|
|
1637
|
+
}
|
|
1638
|
+
function ensureContained(rootPath, candidatePath, boundary) {
|
|
1639
|
+
const childPath = relative(rootPath, candidatePath);
|
|
1640
|
+
if (childPath === ".." || childPath.startsWith(`..${sep}`) || isAbsolute(childPath)) throw new Error(`Mutation target escaped the ${boundary} root.`);
|
|
1641
|
+
}
|
|
1642
|
+
async function readLstat(path) {
|
|
1643
|
+
try {
|
|
1644
|
+
return await lstat(path);
|
|
1645
|
+
} catch (error) {
|
|
1646
|
+
if (isMissingPath(error)) return null;
|
|
1647
|
+
throw error;
|
|
1648
|
+
}
|
|
1649
|
+
}
|
|
1650
|
+
async function assertNearestExistingAncestor(lexicalRoot, physicalRoot, candidatePath) {
|
|
1651
|
+
let currentPath = candidatePath;
|
|
1652
|
+
while (true) {
|
|
1653
|
+
if (await readLstat(currentPath)) {
|
|
1654
|
+
let physicalPath;
|
|
1655
|
+
try {
|
|
1656
|
+
physicalPath = await realpath(currentPath);
|
|
1657
|
+
} catch (error) {
|
|
1658
|
+
throw new Error("Mutation target could not be resolved inside the physical root.", { cause: error });
|
|
1659
|
+
}
|
|
1660
|
+
ensureContained(physicalRoot, physicalPath, "physical");
|
|
1661
|
+
return;
|
|
1662
|
+
}
|
|
1663
|
+
if (currentPath === lexicalRoot) throw new Error("Mutation root no longer exists.");
|
|
1664
|
+
const parentPath = dirname$1(currentPath);
|
|
1665
|
+
ensureContained(lexicalRoot, parentPath, "lexical");
|
|
1666
|
+
currentPath = parentPath;
|
|
1667
|
+
}
|
|
1668
|
+
}
|
|
1669
|
+
async function resolveMutationTarget(input) {
|
|
1670
|
+
const normalizedRelativePath = input.relativePath.replace(/\\/g, "/");
|
|
1671
|
+
if (!normalizedRelativePath || normalizedRelativePath.includes("\0") || isAbsolute(normalizedRelativePath) || /^[A-Za-z]:\//.test(normalizedRelativePath) || normalizedRelativePath.split("/").includes("..")) throw new Error("Mutation target must be a non-empty root-relative path.");
|
|
1672
|
+
const lexicalRoot = resolve(input.rootPath);
|
|
1673
|
+
const targetPath = resolve(lexicalRoot, normalizedRelativePath);
|
|
1674
|
+
ensureContained(lexicalRoot, targetPath, "lexical");
|
|
1675
|
+
if (targetPath === lexicalRoot) throw new Error("Mutation target cannot be the ownership root.");
|
|
1676
|
+
return {
|
|
1677
|
+
lexicalRoot,
|
|
1678
|
+
physicalRoot: await realpath(lexicalRoot),
|
|
1679
|
+
targetPath
|
|
1680
|
+
};
|
|
1681
|
+
}
|
|
1682
|
+
async function assertMutationTarget(target) {
|
|
1683
|
+
await assertNearestExistingAncestor(target.lexicalRoot, target.physicalRoot, target.targetPath);
|
|
1684
|
+
if ((await readLstat(target.targetPath))?.isSymbolicLink()) throw new Error("Mutation target escaped the physical root through a symbolic link.");
|
|
1685
|
+
}
|
|
1686
|
+
async function runResolvedPhysicalReactivePathMutation(input, mutation, settle = settleReactivePathMutation) {
|
|
1687
|
+
const target = await resolveMutationTarget(input);
|
|
1688
|
+
await assertMutationTarget(target);
|
|
1689
|
+
const outcome = await mutation(target).then((value) => ({
|
|
1690
|
+
success: true,
|
|
1691
|
+
value
|
|
1692
|
+
}), (error) => ({
|
|
1693
|
+
success: false,
|
|
1694
|
+
error
|
|
1695
|
+
}));
|
|
1696
|
+
const finalizationFailures = [];
|
|
1697
|
+
await assertMutationTarget(target).catch((error) => finalizationFailures.push(error));
|
|
1698
|
+
await settle(target.targetPath).catch((error) => finalizationFailures.push(error));
|
|
1699
|
+
if (!outcome.success) {
|
|
1700
|
+
if (finalizationFailures.length > 0) throw new AggregateError([outcome.error, ...finalizationFailures], "Physical/reactive mutation and settlement failed.");
|
|
1701
|
+
throw outcome.error;
|
|
1702
|
+
}
|
|
1703
|
+
if (finalizationFailures.length > 0) throw new AggregateError(finalizationFailures, "Physical/reactive mutation settlement failed.");
|
|
1704
|
+
return outcome.value;
|
|
1705
|
+
}
|
|
1706
|
+
/**
|
|
1707
|
+
* Write one UTF-8 file inside an existing physical root and synchronously settle reactive readers.
|
|
1708
|
+
*
|
|
1709
|
+
* Existing ancestors and the final target are resolved immediately before the write. This rejects
|
|
1710
|
+
* observed symlink escapes but cannot make path checks race-free against concurrent filesystem
|
|
1711
|
+
* replacement on platforms without a directory-handle-relative write primitive.
|
|
1396
1712
|
*/
|
|
1397
|
-
function
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1713
|
+
async function writePhysicalReactiveFile(input) {
|
|
1714
|
+
await runResolvedPhysicalReactivePathMutation(input, async (target) => {
|
|
1715
|
+
await mkdir$1(dirname$1(target.targetPath), { recursive: true });
|
|
1716
|
+
await assertMutationTarget(target);
|
|
1717
|
+
await writeFile$1(target.targetPath, input.content, "utf8");
|
|
1718
|
+
}, (targetPath) => settleReactiveFileWrite(targetPath, input.content));
|
|
1719
|
+
}
|
|
1720
|
+
/** Create one root-confined directory and synchronously settle overlapping reactive readers. */
|
|
1721
|
+
async function createPhysicalReactiveDirectory(input) {
|
|
1722
|
+
await runResolvedPhysicalReactivePathMutation(input, async (target) => {
|
|
1723
|
+
await mkdir$1(target.targetPath, { recursive: true });
|
|
1724
|
+
});
|
|
1725
|
+
}
|
|
1726
|
+
/** Remove one root-confined file or directory and synchronously settle descendant projections. */
|
|
1727
|
+
async function removePhysicalReactivePath(input) {
|
|
1728
|
+
await runResolvedPhysicalReactivePathMutation(input, async (target) => {
|
|
1729
|
+
await rm(target.targetPath, {
|
|
1730
|
+
recursive: true,
|
|
1731
|
+
force: true
|
|
1732
|
+
});
|
|
1733
|
+
});
|
|
1413
1734
|
}
|
|
1414
1735
|
/**
|
|
1415
|
-
*
|
|
1736
|
+
* Guard and settle a mutation performed by an external owner such as the OpenSpec CLI.
|
|
1737
|
+
*
|
|
1738
|
+
* The pre/post checks reject observed symlink escape, but cannot close replacement races between
|
|
1739
|
+
* checks and the external process. Hard-link aliases are likewise not identifiable from the path.
|
|
1416
1740
|
*/
|
|
1417
|
-
function
|
|
1418
|
-
return
|
|
1741
|
+
async function runPhysicalReactivePathMutation(input, mutation) {
|
|
1742
|
+
return runResolvedPhysicalReactivePathMutation(input, () => mutation());
|
|
1419
1743
|
}
|
|
1420
1744
|
|
|
1421
1745
|
//#endregion
|
|
@@ -1501,6 +1825,16 @@ var Validator = class {
|
|
|
1501
1825
|
//#endregion
|
|
1502
1826
|
//#region src/adapter.ts
|
|
1503
1827
|
/**
|
|
1828
|
+
* Orthogonal intents (updated 2026-07-23 Asia/Shanghai):
|
|
1829
|
+
* 1. Read and mutate one CLI-selected planning root through reactive filesystem APIs.
|
|
1830
|
+
* 2. Project Specs, Changes, Archives, and schema-neutral entity files.
|
|
1831
|
+
* 3. Keep tracked workflow tasks distinct from document checklist analytics.
|
|
1832
|
+
* 4. Preserve filesystem provenance and mutation boundaries for server consumers.
|
|
1833
|
+
* 5. Expose one bounded Change row projection for progressive list delivery without warming workflow details.
|
|
1834
|
+
*
|
|
1835
|
+
* Original request (2026-07-15): "Split formal tracked progress, document checklist statistics, and Apply instruction progress into non-interchangeable facts."
|
|
1836
|
+
*/
|
|
1837
|
+
/**
|
|
1504
1838
|
* OpenSpec filesystem adapter
|
|
1505
1839
|
* Handles reading, writing, and managing OpenSpec files
|
|
1506
1840
|
*/
|
|
@@ -1563,29 +1897,33 @@ var OpenSpecAdapter = class {
|
|
|
1563
1897
|
exclude: ["archive"]
|
|
1564
1898
|
});
|
|
1565
1899
|
}
|
|
1900
|
+
/** Read one active Change row without resolving workflow Status, Apply, or artifact instructions. */
|
|
1901
|
+
async readChangeMeta(id) {
|
|
1902
|
+
const canonicalId = requireCanonicalOpenSpecEntityId(id, "changeId");
|
|
1903
|
+
const changeDir = join(this.changesDir, canonicalId);
|
|
1904
|
+
const [change, taskProjection, timeInfo] = await Promise.all([
|
|
1905
|
+
this.readChange(canonicalId),
|
|
1906
|
+
this.readChangeTaskProjection(canonicalId),
|
|
1907
|
+
this.getFileTimeInfo(changeDir)
|
|
1908
|
+
]);
|
|
1909
|
+
return {
|
|
1910
|
+
id: canonicalId,
|
|
1911
|
+
name: change?.name ?? canonicalId,
|
|
1912
|
+
trackedTaskProgress: taskProjection.trackedTaskProgress,
|
|
1913
|
+
documentChecklistSummary: taskProjection.documentChecklistSummary,
|
|
1914
|
+
createdAt: timeInfo?.createdAt ?? 0,
|
|
1915
|
+
updatedAt: timeInfo?.updatedAt ?? 0
|
|
1916
|
+
};
|
|
1917
|
+
}
|
|
1566
1918
|
/**
|
|
1567
|
-
* List changes with metadata
|
|
1919
|
+
* List changes with metadata, separated task projections, and time info.
|
|
1568
1920
|
* Returns every change directory, including schema-specific layouts that
|
|
1569
1921
|
* don't use proposal.md/tasks.md.
|
|
1570
1922
|
* Sorted by updatedAt descending (most recent first)
|
|
1571
1923
|
*/
|
|
1572
1924
|
async listChangesWithMeta() {
|
|
1573
1925
|
const ids = await this.listChanges();
|
|
1574
|
-
return (await Promise.all(ids.map(
|
|
1575
|
-
const changeDir = join(this.changesDir, id);
|
|
1576
|
-
const [change, taskProjection, timeInfo] = await Promise.all([
|
|
1577
|
-
this.readChange(id),
|
|
1578
|
-
this.readChangeTaskProjection(id),
|
|
1579
|
-
this.getFileTimeInfo(changeDir)
|
|
1580
|
-
]);
|
|
1581
|
-
return {
|
|
1582
|
-
id,
|
|
1583
|
-
name: change?.name ?? id,
|
|
1584
|
-
progress: taskProjection.progress,
|
|
1585
|
-
createdAt: timeInfo?.createdAt ?? 0,
|
|
1586
|
-
updatedAt: timeInfo?.updatedAt ?? 0
|
|
1587
|
-
};
|
|
1588
|
-
}))).sort((a, b) => b.updatedAt - a.updatedAt);
|
|
1926
|
+
return (await Promise.all(ids.map((id) => this.readChangeMeta(id)))).sort((a, b) => b.updatedAt - a.updatedAt);
|
|
1589
1927
|
}
|
|
1590
1928
|
async listArchivedChanges() {
|
|
1591
1929
|
return reactiveReadDir(this.archiveDir, { directoriesOnly: true });
|
|
@@ -1604,7 +1942,8 @@ var OpenSpecAdapter = class {
|
|
|
1604
1942
|
return {
|
|
1605
1943
|
id,
|
|
1606
1944
|
name: id,
|
|
1607
|
-
|
|
1945
|
+
trackedTaskProgress: taskProjection.trackedTaskProgress,
|
|
1946
|
+
documentChecklistSummary: taskProjection.documentChecklistSummary,
|
|
1608
1947
|
createdAt: timeInfo?.createdAt ?? 0,
|
|
1609
1948
|
updatedAt: timeInfo?.updatedAt ?? 0
|
|
1610
1949
|
};
|
|
@@ -1632,7 +1971,8 @@ var OpenSpecAdapter = class {
|
|
|
1632
1971
|
}
|
|
1633
1972
|
}
|
|
1634
1973
|
async readSpecRaw(specId) {
|
|
1635
|
-
|
|
1974
|
+
const canonicalSpecId = requireCanonicalOpenSpecEntityId(specId, "specId");
|
|
1975
|
+
return reactiveReadFile(join(this.specsDir, canonicalSpecId, "spec.md"));
|
|
1636
1976
|
}
|
|
1637
1977
|
async readChange(changeId) {
|
|
1638
1978
|
try {
|
|
@@ -1647,28 +1987,33 @@ var OpenSpecAdapter = class {
|
|
|
1647
1987
|
}
|
|
1648
1988
|
}
|
|
1649
1989
|
async readChangeFiles(changeId) {
|
|
1650
|
-
const
|
|
1990
|
+
const canonicalChangeId = requireCanonicalOpenSpecEntityId(changeId, "changeId");
|
|
1991
|
+
const changeRoot = join(this.changesDir, canonicalChangeId);
|
|
1651
1992
|
return this.readFilesUnderRoot(changeRoot);
|
|
1652
1993
|
}
|
|
1653
1994
|
async readArchivedChangeFiles(changeId) {
|
|
1654
|
-
const
|
|
1995
|
+
const canonicalChangeId = requireCanonicalOpenSpecEntityId(changeId, "changeId");
|
|
1996
|
+
const archiveRoot = join(this.archiveDir, canonicalChangeId);
|
|
1655
1997
|
return this.readFilesUnderRoot(archiveRoot);
|
|
1656
1998
|
}
|
|
1657
1999
|
async readChangeTaskProjection(changeId) {
|
|
1658
|
-
|
|
2000
|
+
const canonicalChangeId = requireCanonicalOpenSpecEntityId(changeId, "changeId");
|
|
2001
|
+
return this.readEntityTaskProjection(join(this.changesDir, canonicalChangeId));
|
|
1659
2002
|
}
|
|
1660
2003
|
async readArchivedChangeTaskProjection(changeId) {
|
|
1661
|
-
|
|
2004
|
+
const canonicalChangeId = requireCanonicalOpenSpecEntityId(changeId, "changeId");
|
|
2005
|
+
return this.readEntityTaskProjection(join(this.archiveDir, canonicalChangeId));
|
|
1662
2006
|
}
|
|
1663
2007
|
async readEntityDetail(stage, id, options = {}) {
|
|
1664
|
-
const
|
|
2008
|
+
const canonicalId = requireCanonicalOpenSpecEntityId(id, "changeId");
|
|
2009
|
+
const root = stage === "change" ? join(this.changesDir, canonicalId) : join(this.archiveDir, canonicalId);
|
|
1665
2010
|
const files = await this.readFilesUnderRoot(root);
|
|
1666
2011
|
if (files.length === 0) {
|
|
1667
2012
|
if (!(await reactiveStat(root))?.isDirectory) return null;
|
|
1668
2013
|
}
|
|
1669
2014
|
return buildOpsxEntityDetail({
|
|
1670
2015
|
stage,
|
|
1671
|
-
id,
|
|
2016
|
+
id: canonicalId,
|
|
1672
2017
|
files,
|
|
1673
2018
|
schemas: options.schemas,
|
|
1674
2019
|
schemaDiagnostics: options.schemaDiagnostics
|
|
@@ -1722,13 +2067,14 @@ var OpenSpecAdapter = class {
|
|
|
1722
2067
|
async readEntityTaskProjection(root) {
|
|
1723
2068
|
const files = await this.readFilesUnderRoot(root);
|
|
1724
2069
|
const metadata = parseOpsxEntityMetadata(files.find((file) => file.type === "file" && file.path === ".openspec.yaml")?.content ?? null);
|
|
1725
|
-
return
|
|
2070
|
+
return projectTaskProjectionsFromMarkdownFiles(files, {
|
|
1726
2071
|
schemaDetail: metadata.schemaName ? await this.readProjectSchemaDetail(metadata.schemaName) : null,
|
|
1727
2072
|
hasSchemaMetadata: Boolean(metadata.schemaName)
|
|
1728
2073
|
});
|
|
1729
2074
|
}
|
|
1730
2075
|
async readChangeRaw(changeId) {
|
|
1731
|
-
const
|
|
2076
|
+
const canonicalChangeId = requireCanonicalOpenSpecEntityId(changeId, "changeId");
|
|
2077
|
+
const changeDir = join(this.changesDir, canonicalChangeId);
|
|
1732
2078
|
const proposalPath = join(changeDir, "proposal.md");
|
|
1733
2079
|
const tasksPath = join(changeDir, "tasks.md");
|
|
1734
2080
|
const designPath = join(changeDir, "design.md");
|
|
@@ -1779,7 +2125,8 @@ var OpenSpecAdapter = class {
|
|
|
1779
2125
|
* Read raw archived change files (reactive)
|
|
1780
2126
|
*/
|
|
1781
2127
|
async readArchivedChangeRaw(changeId) {
|
|
1782
|
-
const
|
|
2128
|
+
const canonicalChangeId = requireCanonicalOpenSpecEntityId(changeId, "changeId");
|
|
2129
|
+
const archiveChangeDir = join(this.archiveDir, canonicalChangeId);
|
|
1783
2130
|
const proposalPath = join(archiveChangeDir, "proposal.md");
|
|
1784
2131
|
const tasksPath = join(archiveChangeDir, "tasks.md");
|
|
1785
2132
|
const designPath = join(archiveChangeDir, "design.md");
|
|
@@ -1799,26 +2146,35 @@ var OpenSpecAdapter = class {
|
|
|
1799
2146
|
};
|
|
1800
2147
|
}
|
|
1801
2148
|
async writeSpec(specId, content) {
|
|
1802
|
-
const
|
|
1803
|
-
await
|
|
1804
|
-
|
|
2149
|
+
const canonicalSpecId = requireCanonicalOpenSpecEntityId(specId, "specId");
|
|
2150
|
+
await writePhysicalReactiveFile({
|
|
2151
|
+
rootPath: this.projectDir,
|
|
2152
|
+
relativePath: join("openspec", "specs", canonicalSpecId, "spec.md"),
|
|
2153
|
+
content
|
|
2154
|
+
});
|
|
1805
2155
|
}
|
|
1806
2156
|
async writeChange(changeId, proposal, tasks) {
|
|
1807
|
-
const
|
|
1808
|
-
await
|
|
1809
|
-
|
|
1810
|
-
|
|
2157
|
+
const canonicalChangeId = requireCanonicalOpenSpecEntityId(changeId, "changeId");
|
|
2158
|
+
await writePhysicalReactiveFile({
|
|
2159
|
+
rootPath: this.projectDir,
|
|
2160
|
+
relativePath: join("openspec", "changes", canonicalChangeId, "proposal.md"),
|
|
2161
|
+
content: proposal
|
|
2162
|
+
});
|
|
2163
|
+
if (tasks !== void 0) await writePhysicalReactiveFile({
|
|
2164
|
+
rootPath: this.projectDir,
|
|
2165
|
+
relativePath: join("openspec", "changes", canonicalChangeId, "tasks.md"),
|
|
2166
|
+
content: tasks
|
|
2167
|
+
});
|
|
1811
2168
|
}
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
}
|
|
2169
|
+
/** Write one validated Change or Archive entity file through the shared physical/reactive owner. */
|
|
2170
|
+
async writeEntityFile(stage, changeId, path, content) {
|
|
2171
|
+
const canonicalChangeId = requireCanonicalOpenSpecEntityId(changeId, "changeId");
|
|
2172
|
+
const relativePath = requireOpenSpecEntityRelativePath(path, "path");
|
|
2173
|
+
await writePhysicalReactiveFile({
|
|
2174
|
+
rootPath: this.projectDir,
|
|
2175
|
+
relativePath: join(getOpsxEntityRootRelativePath(stage, canonicalChangeId), relativePath),
|
|
2176
|
+
content
|
|
2177
|
+
});
|
|
1822
2178
|
}
|
|
1823
2179
|
async init() {
|
|
1824
2180
|
await mkdir(this.specsDir, { recursive: true });
|
|
@@ -1835,36 +2191,6 @@ This project uses OpenSpec for spec-driven development.
|
|
|
1835
2191
|
- \`changes/archive/\` - Completed changes
|
|
1836
2192
|
`, "utf-8");
|
|
1837
2193
|
}
|
|
1838
|
-
/**
|
|
1839
|
-
* Toggle a task's completion status in tasks.md
|
|
1840
|
-
* @param changeId - The change ID
|
|
1841
|
-
* @param taskIndex - 1-based task index
|
|
1842
|
-
* @param completed - New completion status
|
|
1843
|
-
*/
|
|
1844
|
-
async toggleTask(changeId, taskIndex, completed) {
|
|
1845
|
-
try {
|
|
1846
|
-
const tasksPath = join(this.changesDir, changeId, "tasks.md");
|
|
1847
|
-
const lines = (await readFile(tasksPath, "utf-8")).split("\n");
|
|
1848
|
-
let currentTaskIndex = 0;
|
|
1849
|
-
for (let i = 0; i < lines.length; i++) {
|
|
1850
|
-
const taskMatch = lines[i].match(/^([-*]\s+)\[([ xX])\](\s+.*)$/);
|
|
1851
|
-
if (taskMatch) {
|
|
1852
|
-
currentTaskIndex++;
|
|
1853
|
-
if (currentTaskIndex === taskIndex) {
|
|
1854
|
-
const prefix = taskMatch[1];
|
|
1855
|
-
const suffix = taskMatch[3];
|
|
1856
|
-
lines[i] = `${prefix}${completed ? "[x]" : "[ ]"}${suffix}`;
|
|
1857
|
-
break;
|
|
1858
|
-
}
|
|
1859
|
-
}
|
|
1860
|
-
}
|
|
1861
|
-
if (currentTaskIndex < taskIndex) return false;
|
|
1862
|
-
await writeFile(tasksPath, lines.join("\n"), "utf-8");
|
|
1863
|
-
return true;
|
|
1864
|
-
} catch {
|
|
1865
|
-
return false;
|
|
1866
|
-
}
|
|
1867
|
-
}
|
|
1868
2194
|
async validateSpec(specId) {
|
|
1869
2195
|
const spec = await this.readSpec(specId);
|
|
1870
2196
|
if (!spec) return {
|
|
@@ -1898,8 +2224,8 @@ This project uses OpenSpec for spec-driven development.
|
|
|
1898
2224
|
const validSpecs = specs.filter((s) => s !== null);
|
|
1899
2225
|
const validChanges = changes.filter((c) => c !== null);
|
|
1900
2226
|
const totalRequirements = validSpecs.reduce((sum, s) => sum + s.requirements.length, 0);
|
|
1901
|
-
const totalTasks = validChanges.reduce((sum,
|
|
1902
|
-
const completedTasks = validChanges.reduce((sum,
|
|
2227
|
+
const totalTasks = validChanges.reduce((sum, change) => sum + change.trackedTaskProgress.total, 0);
|
|
2228
|
+
const completedTasks = validChanges.reduce((sum, change) => sum + change.trackedTaskProgress.completed, 0);
|
|
1903
2229
|
return {
|
|
1904
2230
|
specs: validSpecs,
|
|
1905
2231
|
changes: validChanges,
|
|
@@ -1918,135 +2244,150 @@ This project uses OpenSpec for spec-driven development.
|
|
|
1918
2244
|
};
|
|
1919
2245
|
|
|
1920
2246
|
//#endregion
|
|
1921
|
-
//#region src/
|
|
2247
|
+
//#region src/planning-config.ts
|
|
1922
2248
|
/**
|
|
1923
|
-
*
|
|
2249
|
+
* Orthogonal intents (updated 2026-07-27 Asia/Shanghai):
|
|
2250
|
+
* 1. Define distinct Project Binding, Active Root, and reactive Environment Global config projections.
|
|
2251
|
+
* 2. Inspect launch-project Store/Reference declarations without replacing CLI Root Context truth.
|
|
2252
|
+
* 3. Update only binding fields while preserving unrelated YAML fields and comments.
|
|
2253
|
+
* 4. Describe a typed launch-write result separately from asynchronous Root Context convergence.
|
|
2254
|
+
* 5. Separate CLI-owned Environment Global facts from the file-native editable document owner.
|
|
1924
2255
|
*
|
|
1925
|
-
*
|
|
1926
|
-
* -
|
|
1927
|
-
* -
|
|
1928
|
-
* - Task: A trackable work item within a change
|
|
1929
|
-
*
|
|
1930
|
-
* @module schemas
|
|
1931
|
-
*/
|
|
1932
|
-
/**
|
|
1933
|
-
* File metadata for a change directory entry.
|
|
1934
|
-
*/
|
|
1935
|
-
const ChangeFileSchema = z.object({
|
|
1936
|
-
path: z.string(),
|
|
1937
|
-
type: z.enum(["file", "directory"]),
|
|
1938
|
-
content: z.string().optional(),
|
|
1939
|
-
size: z.number().optional(),
|
|
1940
|
-
mime: z.string().optional(),
|
|
1941
|
-
previewKind: z.enum(FILE_PREVIEW_KINDS).optional()
|
|
1942
|
-
});
|
|
1943
|
-
const ScenarioStepKeywordSchema = z.enum([
|
|
1944
|
-
"GIVEN",
|
|
1945
|
-
"WHEN",
|
|
1946
|
-
"THEN",
|
|
1947
|
-
"AND",
|
|
1948
|
-
"BUT"
|
|
1949
|
-
]);
|
|
1950
|
-
const ScenarioStepSchema = z.object({
|
|
1951
|
-
keyword: ScenarioStepKeywordSchema,
|
|
1952
|
-
contentMarkdown: z.string(),
|
|
1953
|
-
rawText: z.string()
|
|
1954
|
-
});
|
|
1955
|
-
/**
|
|
1956
|
-
* A requirement within a specification.
|
|
1957
|
-
* Requirements should use RFC 2119 keywords (SHALL, MUST, etc.)
|
|
1958
|
-
*/
|
|
1959
|
-
const RequirementSchema = z.object({
|
|
1960
|
-
id: z.string(),
|
|
1961
|
-
title: z.string(),
|
|
1962
|
-
bodyMarkdown: z.string(),
|
|
1963
|
-
text: z.string(),
|
|
1964
|
-
scenarios: z.array(z.object({
|
|
1965
|
-
title: z.string(),
|
|
1966
|
-
bodyMarkdown: z.string(),
|
|
1967
|
-
rawText: z.string(),
|
|
1968
|
-
steps: z.array(ScenarioStepSchema).optional()
|
|
1969
|
-
}))
|
|
1970
|
-
});
|
|
1971
|
-
/**
|
|
1972
|
-
* A specification document.
|
|
1973
|
-
* Located at: openspec/specs/{id}/spec.md
|
|
1974
|
-
*/
|
|
1975
|
-
const SpecSchema = z.object({
|
|
1976
|
-
id: z.string(),
|
|
1977
|
-
name: z.string(),
|
|
1978
|
-
overview: z.string(),
|
|
1979
|
-
requirements: z.array(RequirementSchema),
|
|
1980
|
-
metadata: z.object({
|
|
1981
|
-
version: z.string().default("1.0.0"),
|
|
1982
|
-
format: z.literal("openspec").default("openspec"),
|
|
1983
|
-
sourcePath: z.string().optional()
|
|
1984
|
-
}).optional()
|
|
1985
|
-
});
|
|
1986
|
-
/**
|
|
1987
|
-
* A delta describes changes to a spec within a change proposal.
|
|
1988
|
-
* Deltas track which specs are affected and how.
|
|
1989
|
-
*/
|
|
1990
|
-
const DeltaOperationType = z.enum([
|
|
1991
|
-
"ADDED",
|
|
1992
|
-
"MODIFIED",
|
|
1993
|
-
"REMOVED",
|
|
1994
|
-
"RENAMED"
|
|
1995
|
-
]);
|
|
1996
|
-
const DeltaSchema = z.object({
|
|
1997
|
-
spec: z.string(),
|
|
1998
|
-
operation: DeltaOperationType,
|
|
1999
|
-
description: z.string(),
|
|
2000
|
-
requirement: RequirementSchema.optional(),
|
|
2001
|
-
requirements: z.array(RequirementSchema).optional(),
|
|
2002
|
-
rename: z.object({
|
|
2003
|
-
from: z.string(),
|
|
2004
|
-
to: z.string()
|
|
2005
|
-
}).optional()
|
|
2006
|
-
});
|
|
2007
|
-
/**
|
|
2008
|
-
* A task within a change proposal.
|
|
2009
|
-
* Tasks are parsed from tasks.md using checkbox syntax: - [ ] or - [x]
|
|
2256
|
+
* Original request (2026-07-15): "Config ownership separates launch-project binding, active-root config, and environment-global config."
|
|
2257
|
+
* Original request (2026-07-18): "Profile/Drift must refresh with external environment config changes."
|
|
2258
|
+
* Derived requirement (2026-07-19): "Project Binding returns launch-write, root preview, and transition evidence."
|
|
2010
2259
|
*/
|
|
2011
|
-
|
|
2012
|
-
|
|
2013
|
-
|
|
2014
|
-
|
|
2015
|
-
|
|
2016
|
-
|
|
2017
|
-
|
|
2018
|
-
|
|
2019
|
-
|
|
2020
|
-
*/
|
|
2021
|
-
const
|
|
2022
|
-
|
|
2023
|
-
|
|
2024
|
-
|
|
2025
|
-
|
|
2026
|
-
|
|
2027
|
-
|
|
2028
|
-
|
|
2029
|
-
|
|
2030
|
-
|
|
2031
|
-
|
|
2032
|
-
|
|
2033
|
-
|
|
2034
|
-
|
|
2035
|
-
|
|
2036
|
-
|
|
2037
|
-
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
|
|
2043
|
-
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
|
|
2047
|
-
|
|
2048
|
-
|
|
2049
|
-
|
|
2260
|
+
/** Runtime schema for JSON values accepted by OpenSpec global configuration. */
|
|
2261
|
+
const PlanningConfigJsonValueSchema = z.lazy(() => z.union([
|
|
2262
|
+
z.string(),
|
|
2263
|
+
z.number(),
|
|
2264
|
+
z.boolean(),
|
|
2265
|
+
z.null(),
|
|
2266
|
+
z.array(PlanningConfigJsonValueSchema),
|
|
2267
|
+
z.record(z.string(), PlanningConfigJsonValueSchema)
|
|
2268
|
+
]));
|
|
2269
|
+
/** Runtime schema for the complete environment-global OpenSpec configuration document. */
|
|
2270
|
+
const EnvironmentGlobalConfigValueSchema = z.record(z.string(), PlanningConfigJsonValueSchema);
|
|
2271
|
+
/** Runtime schema for one project-declared Store Reference entry. */
|
|
2272
|
+
const PlanningConfigReferenceSchema = z.object({
|
|
2273
|
+
id: z.string().min(1),
|
|
2274
|
+
remote: z.string().min(1).optional()
|
|
2275
|
+
}).strict();
|
|
2276
|
+
/** Runtime schema for project binding mutations. */
|
|
2277
|
+
const ProjectBindingUpdateSchema = z.object({
|
|
2278
|
+
store: z.string().min(1).nullable().optional(),
|
|
2279
|
+
references: z.array(PlanningConfigReferenceSchema).nullable().optional()
|
|
2280
|
+
}).refine((value) => value.store !== void 0 || value.references !== void 0, { message: "At least one Project Binding field is required." });
|
|
2281
|
+
function isRecord(value) {
|
|
2282
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
2283
|
+
}
|
|
2284
|
+
function inspectReference(value) {
|
|
2285
|
+
if (typeof value === "string" && value.length > 0) return { id: value };
|
|
2286
|
+
if (!isRecord(value) || typeof value.id !== "string" || value.id.length === 0) return null;
|
|
2287
|
+
if (value.remote === void 0) return { id: value.id };
|
|
2288
|
+
if (typeof value.remote !== "string" || value.remote.length === 0) return null;
|
|
2289
|
+
return {
|
|
2290
|
+
id: value.id,
|
|
2291
|
+
remote: value.remote
|
|
2292
|
+
};
|
|
2293
|
+
}
|
|
2294
|
+
/** Inspect only launch-project binding declarations; Root Context remains the effective projection. */
|
|
2295
|
+
function inspectProjectBinding(content) {
|
|
2296
|
+
const absent = {
|
|
2297
|
+
store: {
|
|
2298
|
+
state: "absent",
|
|
2299
|
+
id: null
|
|
2300
|
+
},
|
|
2301
|
+
references: {
|
|
2302
|
+
state: "absent",
|
|
2303
|
+
entries: []
|
|
2304
|
+
},
|
|
2305
|
+
diagnostics: []
|
|
2306
|
+
};
|
|
2307
|
+
if (content === null || content.trim().length === 0) return absent;
|
|
2308
|
+
const document = parseDocument(content);
|
|
2309
|
+
if (document.errors.length > 0) return {
|
|
2310
|
+
...absent,
|
|
2311
|
+
diagnostics: [{
|
|
2312
|
+
code: "config-unparseable",
|
|
2313
|
+
message: document.errors.map((error) => error.message).join("\n")
|
|
2314
|
+
}]
|
|
2315
|
+
};
|
|
2316
|
+
const raw = document.toJS();
|
|
2317
|
+
if (raw === null) return absent;
|
|
2318
|
+
if (!isRecord(raw)) return {
|
|
2319
|
+
...absent,
|
|
2320
|
+
diagnostics: [{
|
|
2321
|
+
code: "config-not-mapping",
|
|
2322
|
+
message: "OpenSpec config must be a YAML mapping."
|
|
2323
|
+
}]
|
|
2324
|
+
};
|
|
2325
|
+
const diagnostics = [];
|
|
2326
|
+
let store = {
|
|
2327
|
+
state: "absent",
|
|
2328
|
+
id: null
|
|
2329
|
+
};
|
|
2330
|
+
if (raw.store !== void 0) if (typeof raw.store === "string" && raw.store.length > 0) store = {
|
|
2331
|
+
state: "declared",
|
|
2332
|
+
id: raw.store
|
|
2333
|
+
};
|
|
2334
|
+
else {
|
|
2335
|
+
store = {
|
|
2336
|
+
state: "invalid",
|
|
2337
|
+
id: null
|
|
2338
|
+
};
|
|
2339
|
+
diagnostics.push({
|
|
2340
|
+
code: "store-not-string",
|
|
2341
|
+
message: "The store field must be one non-empty Store id string."
|
|
2342
|
+
});
|
|
2343
|
+
}
|
|
2344
|
+
let references = {
|
|
2345
|
+
state: "absent",
|
|
2346
|
+
entries: []
|
|
2347
|
+
};
|
|
2348
|
+
if (raw.references !== void 0) if (!Array.isArray(raw.references)) {
|
|
2349
|
+
references = {
|
|
2350
|
+
state: "invalid",
|
|
2351
|
+
entries: []
|
|
2352
|
+
};
|
|
2353
|
+
diagnostics.push({
|
|
2354
|
+
code: "references-not-array",
|
|
2355
|
+
message: "The references field must be an array of Store declarations."
|
|
2356
|
+
});
|
|
2357
|
+
} else {
|
|
2358
|
+
const entries = [];
|
|
2359
|
+
let invalid = false;
|
|
2360
|
+
for (const value of raw.references) {
|
|
2361
|
+
const reference = inspectReference(value);
|
|
2362
|
+
if (reference) entries.push(reference);
|
|
2363
|
+
else invalid = true;
|
|
2364
|
+
}
|
|
2365
|
+
references = {
|
|
2366
|
+
state: invalid ? "invalid" : "declared",
|
|
2367
|
+
entries
|
|
2368
|
+
};
|
|
2369
|
+
if (invalid) diagnostics.push({
|
|
2370
|
+
code: "reference-entry-invalid",
|
|
2371
|
+
message: "One or more Reference declarations are not a Store id or { id, remote }."
|
|
2372
|
+
});
|
|
2373
|
+
}
|
|
2374
|
+
return {
|
|
2375
|
+
store,
|
|
2376
|
+
references,
|
|
2377
|
+
diagnostics
|
|
2378
|
+
};
|
|
2379
|
+
}
|
|
2380
|
+
/** Update only `store` and `references`, retaining all unrelated YAML nodes and comments. */
|
|
2381
|
+
function updateProjectBindingContent(content, update) {
|
|
2382
|
+
const document = parseDocument(content?.trim() ? content : "{}\n");
|
|
2383
|
+
if (document.errors.length > 0) throw new Error(document.errors.map((error) => error.message).join("\n"));
|
|
2384
|
+
if (document.contents !== null && !isMap(document.contents)) throw new Error("OpenSpec config must be a YAML mapping before Project Binding can be edited.");
|
|
2385
|
+
if (update.store !== void 0) if (update.store === null) document.delete("store");
|
|
2386
|
+
else document.set("store", update.store);
|
|
2387
|
+
if (update.references !== void 0) if (update.references === null) document.delete("references");
|
|
2388
|
+
else document.set("references", update.references.map((reference) => reference.remote === void 0 ? reference.id : reference));
|
|
2389
|
+
return document.toString({ lineWidth: 0 });
|
|
2390
|
+
}
|
|
2050
2391
|
|
|
2051
2392
|
//#endregion
|
|
2052
2393
|
//#region src/watcher.ts
|
|
@@ -2327,6 +2668,20 @@ function runBufferedCommand(options) {
|
|
|
2327
2668
|
|
|
2328
2669
|
//#endregion
|
|
2329
2670
|
//#region src/config.ts
|
|
2671
|
+
/**
|
|
2672
|
+
* Orthogonal intents (updated 2026-07-20 Asia/Shanghai):
|
|
2673
|
+
* 1. Define and persist project-scoped OpenSpecUI settings.
|
|
2674
|
+
* 2. Resolve, diagnose, cache, and explicitly invalidate the OpenSpec CLI runner.
|
|
2675
|
+
* 3. Prevent retired in-flight runner resolutions from reclaiming cache ownership.
|
|
2676
|
+
* 4. Preserve reactive config reads and serialized config writes.
|
|
2677
|
+
*
|
|
2678
|
+
* Original request (2026-07-14): "openspec 1.6.0 已经放出,我们需要开始进行适配。"
|
|
2679
|
+
* Independent review correction (2026-07-20): Global CLI installation must retire cached and
|
|
2680
|
+
* in-flight runner authority.
|
|
2681
|
+
*
|
|
2682
|
+
* Compromise: configuration schemas and runtime management remain in one historical physical file;
|
|
2683
|
+
* splitting that public surface is outside the bounded 6.13 correction.
|
|
2684
|
+
*/
|
|
2330
2685
|
const execAsync = promisify(exec);
|
|
2331
2686
|
const execFileAsync = promisify(execFile);
|
|
2332
2687
|
const CLI_PROBE_TIMEOUT_MS = 2e4;
|
|
@@ -3078,6 +3433,7 @@ var ConfigManager = class {
|
|
|
3078
3433
|
configPath;
|
|
3079
3434
|
projectDir;
|
|
3080
3435
|
resolvedRunner = null;
|
|
3436
|
+
resolvedRunnerGeneration = 0;
|
|
3081
3437
|
resolvingRunnerPromise = null;
|
|
3082
3438
|
writeChain = Promise.resolve();
|
|
3083
3439
|
constructor(projectDir) {
|
|
@@ -3205,13 +3561,15 @@ var ConfigManager = class {
|
|
|
3205
3561
|
async resolveCliRunner() {
|
|
3206
3562
|
if (this.resolvedRunner) return this.resolvedRunner;
|
|
3207
3563
|
if (this.resolvingRunnerPromise) return this.resolvingRunnerPromise;
|
|
3208
|
-
|
|
3209
|
-
|
|
3564
|
+
const generation = this.resolvedRunnerGeneration;
|
|
3565
|
+
const resolution = this.resolveCliRunnerUncached().then((runner) => {
|
|
3566
|
+
if (this.resolvedRunnerGeneration === generation && this.resolvingRunnerPromise === resolution) this.resolvedRunner = runner;
|
|
3210
3567
|
return runner;
|
|
3211
3568
|
}).finally(() => {
|
|
3212
|
-
this.resolvingRunnerPromise = null;
|
|
3569
|
+
if (this.resolvingRunnerPromise === resolution) this.resolvingRunnerPromise = null;
|
|
3213
3570
|
});
|
|
3214
|
-
|
|
3571
|
+
this.resolvingRunnerPromise = resolution;
|
|
3572
|
+
return resolution;
|
|
3215
3573
|
}
|
|
3216
3574
|
async resolveCliRunnerUncached() {
|
|
3217
3575
|
const config = await this.readConfig();
|
|
@@ -3244,9 +3602,13 @@ var ConfigManager = class {
|
|
|
3244
3602
|
return this.resolveCliRunner();
|
|
3245
3603
|
}
|
|
3246
3604
|
/**
|
|
3247
|
-
*
|
|
3605
|
+
* Retire the resolved and in-flight CLI runner generation.
|
|
3606
|
+
*
|
|
3607
|
+
* Existing callers still receive their own resolution result, but a retired resolution cannot
|
|
3608
|
+
* repopulate the shared cache or clear a newer pending owner.
|
|
3248
3609
|
*/
|
|
3249
3610
|
invalidateResolvedCliRunner() {
|
|
3611
|
+
this.resolvedRunnerGeneration += 1;
|
|
3250
3612
|
this.resolvedRunner = null;
|
|
3251
3613
|
this.resolvingRunnerPromise = null;
|
|
3252
3614
|
}
|
|
@@ -3729,8 +4091,61 @@ function buildRuntimePackageInstallCommand(input) {
|
|
|
3729
4091
|
}) ?? null;
|
|
3730
4092
|
}
|
|
3731
4093
|
|
|
4094
|
+
//#endregion
|
|
4095
|
+
//#region src/cli-stream-child-owner.ts
|
|
4096
|
+
/** Internal owner for the direct child of one CLI stream. */
|
|
4097
|
+
var CliStreamChildOwner = class {
|
|
4098
|
+
#currentChild = null;
|
|
4099
|
+
#releaseCount = 0;
|
|
4100
|
+
claim(child) {
|
|
4101
|
+
if (this.#currentChild !== null) throw new Error("Cannot claim a second child before releasing the current CLI stream child");
|
|
4102
|
+
this.#currentChild = child;
|
|
4103
|
+
ownersByChild.set(child, this);
|
|
4104
|
+
}
|
|
4105
|
+
owns(child) {
|
|
4106
|
+
return this.#currentChild === child;
|
|
4107
|
+
}
|
|
4108
|
+
release(child) {
|
|
4109
|
+
if (!this.owns(child)) return false;
|
|
4110
|
+
this.#currentChild = null;
|
|
4111
|
+
this.#releaseCount += 1;
|
|
4112
|
+
return true;
|
|
4113
|
+
}
|
|
4114
|
+
get currentChild() {
|
|
4115
|
+
return this.#currentChild;
|
|
4116
|
+
}
|
|
4117
|
+
inspect() {
|
|
4118
|
+
return {
|
|
4119
|
+
currentChild: this.#currentChild,
|
|
4120
|
+
releaseCount: this.#releaseCount
|
|
4121
|
+
};
|
|
4122
|
+
}
|
|
4123
|
+
};
|
|
4124
|
+
const ownersByChild = /* @__PURE__ */ new WeakMap();
|
|
4125
|
+
|
|
3732
4126
|
//#endregion
|
|
3733
4127
|
//#region src/cli-executor.ts
|
|
4128
|
+
/** Failure raised when even forced termination cannot confirm child-process close. */
|
|
4129
|
+
var CliStreamTerminationError = class extends Error {
|
|
4130
|
+
constructor(command) {
|
|
4131
|
+
super(`CLI stream did not close after forced termination: ${command}`);
|
|
4132
|
+
this.name = "CliStreamTerminationError";
|
|
4133
|
+
}
|
|
4134
|
+
};
|
|
4135
|
+
const STREAM_TERMINATION_GRACE_MS = 1e3;
|
|
4136
|
+
const STREAM_FORCE_CLOSE_TIMEOUT_MS = 1e3;
|
|
4137
|
+
function createDeferred() {
|
|
4138
|
+
let resolve$1;
|
|
4139
|
+
let reject;
|
|
4140
|
+
return {
|
|
4141
|
+
promise: new Promise((resolvePromise, rejectPromise) => {
|
|
4142
|
+
resolve$1 = resolvePromise;
|
|
4143
|
+
reject = rejectPromise;
|
|
4144
|
+
}),
|
|
4145
|
+
resolve: resolve$1,
|
|
4146
|
+
reject
|
|
4147
|
+
};
|
|
4148
|
+
}
|
|
3734
4149
|
/**
|
|
3735
4150
|
* CLI 执行器
|
|
3736
4151
|
*
|
|
@@ -3738,9 +4153,11 @@ function buildRuntimePackageInstallCommand(input) {
|
|
|
3738
4153
|
* 所有命令都使用 shell: false,避免 shell 注入风险。
|
|
3739
4154
|
*/
|
|
3740
4155
|
var CliExecutor = class {
|
|
4156
|
+
contracts;
|
|
3741
4157
|
constructor(configManager, projectDir) {
|
|
3742
4158
|
this.configManager = configManager;
|
|
3743
4159
|
this.projectDir = projectDir;
|
|
4160
|
+
this.contracts = new OpenSpecCliContractExecutor((args) => this.execute(args));
|
|
3744
4161
|
}
|
|
3745
4162
|
async buildCommandArray(args) {
|
|
3746
4163
|
return [...await this.configManager.getCliCommand(), ...args];
|
|
@@ -3828,8 +4245,8 @@ var CliExecutor = class {
|
|
|
3828
4245
|
*/
|
|
3829
4246
|
async validate(type, id) {
|
|
3830
4247
|
const args = ["validate"];
|
|
3831
|
-
if (type) args.push(type);
|
|
3832
4248
|
if (id) args.push(id);
|
|
4249
|
+
if (type) args.push("--type", type);
|
|
3833
4250
|
return this.execute(args);
|
|
3834
4251
|
}
|
|
3835
4252
|
/**
|
|
@@ -3858,37 +4275,14 @@ var CliExecutor = class {
|
|
|
3858
4275
|
return this.execute(args);
|
|
3859
4276
|
}
|
|
3860
4277
|
/**
|
|
3861
|
-
* 执行 openspec store list --json
|
|
3862
|
-
*
|
|
3863
|
-
* Beta 功能(Stores)。返回原始 CliResult;数据归类(ok / 异常一数据不兼容 /
|
|
3864
|
-
* 异常二指令变更)由调用方用 classifyStoreCliOutput 完成。这样 CliExecutor 保持单一职责。
|
|
3865
|
-
* Spec: openspec-cli-integration › Stores CLI Query Mapping / Beta Feature Fault Tolerance。
|
|
3866
|
-
*/
|
|
3867
|
-
async listStores() {
|
|
3868
|
-
return this.execute([
|
|
3869
|
-
"store",
|
|
3870
|
-
"list",
|
|
3871
|
-
"--json"
|
|
3872
|
-
]);
|
|
3873
|
-
}
|
|
3874
|
-
/**
|
|
3875
|
-
* 执行 openspec store doctor [--json] [id]
|
|
3876
|
-
*
|
|
3877
|
-
* Beta 功能(Stores)。同 listStores,归类交给调用方。
|
|
3878
|
-
*/
|
|
3879
|
-
async doctorStores(id) {
|
|
3880
|
-
const args = ["store", "doctor"];
|
|
3881
|
-
if (id) args.push(id);
|
|
3882
|
-
args.push("--json");
|
|
3883
|
-
return this.execute(args);
|
|
3884
|
-
}
|
|
3885
|
-
/**
|
|
3886
4278
|
* 流式执行 openspec validate
|
|
3887
4279
|
*/
|
|
3888
|
-
validateStream(
|
|
4280
|
+
validateStream(options, onEvent) {
|
|
3889
4281
|
const args = ["validate"];
|
|
3890
|
-
if (
|
|
3891
|
-
if (
|
|
4282
|
+
if (options.id) args.push(options.id);
|
|
4283
|
+
if (options.type) args.push("--type", options.type);
|
|
4284
|
+
if (options.strict) args.push("--strict");
|
|
4285
|
+
if (options.store !== void 0) args.push("--store", options.store);
|
|
3892
4286
|
return this.executeStream(args, onEvent);
|
|
3893
4287
|
}
|
|
3894
4288
|
/**
|
|
@@ -3920,28 +4314,78 @@ var CliExecutor = class {
|
|
|
3920
4314
|
/**
|
|
3921
4315
|
* 流式执行 CLI 命令
|
|
3922
4316
|
*/
|
|
3923
|
-
|
|
3924
|
-
|
|
3925
|
-
|
|
4317
|
+
executeStream(args, onEvent) {
|
|
4318
|
+
return this.createStreamHandle(async () => this.buildCommandArray(args), onEvent, () => this.configManager.invalidateResolvedCliRunner());
|
|
4319
|
+
}
|
|
4320
|
+
createStreamHandle(resolveCommand, onEvent, retryRunner) {
|
|
4321
|
+
const settlement = createDeferred();
|
|
4322
|
+
let settled = false;
|
|
4323
|
+
let cancelRequested = false;
|
|
4324
|
+
let terminationStarted = false;
|
|
4325
|
+
const childOwner = new CliStreamChildOwner();
|
|
4326
|
+
let activeCommand = "CLI command";
|
|
4327
|
+
let terminationTimer = null;
|
|
4328
|
+
let forceCloseTimer = null;
|
|
4329
|
+
const clearTerminationTimers = () => {
|
|
4330
|
+
if (terminationTimer) clearTimeout(terminationTimer);
|
|
4331
|
+
if (forceCloseTimer) clearTimeout(forceCloseTimer);
|
|
4332
|
+
terminationTimer = null;
|
|
4333
|
+
forceCloseTimer = null;
|
|
4334
|
+
};
|
|
4335
|
+
const settle = (value) => {
|
|
4336
|
+
if (settled) return;
|
|
4337
|
+
settled = true;
|
|
4338
|
+
clearTerminationTimers();
|
|
4339
|
+
onEvent({
|
|
4340
|
+
type: "exit",
|
|
4341
|
+
exitCode: value.exitCode
|
|
4342
|
+
});
|
|
4343
|
+
settlement.resolve(value);
|
|
4344
|
+
};
|
|
4345
|
+
const failTermination = () => {
|
|
4346
|
+
if (settled) return;
|
|
4347
|
+
settled = true;
|
|
4348
|
+
clearTerminationTimers();
|
|
4349
|
+
settlement.reject(new CliStreamTerminationError(activeCommand));
|
|
4350
|
+
};
|
|
4351
|
+
const requestChildTermination = (child) => {
|
|
4352
|
+
if (terminationStarted || settled) return;
|
|
4353
|
+
terminationStarted = true;
|
|
4354
|
+
try {
|
|
4355
|
+
child.kill("SIGTERM");
|
|
4356
|
+
} catch {}
|
|
4357
|
+
terminationTimer = setTimeout(() => {
|
|
4358
|
+
if (settled || !childOwner.owns(child)) return;
|
|
4359
|
+
try {
|
|
4360
|
+
child.kill("SIGKILL");
|
|
4361
|
+
} catch {}
|
|
4362
|
+
forceCloseTimer = setTimeout(() => {
|
|
4363
|
+
if (!settled && childOwner.owns(child)) failTermination();
|
|
4364
|
+
}, STREAM_FORCE_CLOSE_TIMEOUT_MS);
|
|
4365
|
+
}, STREAM_TERMINATION_GRACE_MS);
|
|
4366
|
+
};
|
|
3926
4367
|
const start = async (allowRetry) => {
|
|
3927
|
-
if (
|
|
4368
|
+
if (cancelRequested || settled) return;
|
|
3928
4369
|
let fullCommand;
|
|
3929
4370
|
try {
|
|
3930
|
-
fullCommand = await
|
|
4371
|
+
fullCommand = await resolveCommand();
|
|
3931
4372
|
} catch (err) {
|
|
4373
|
+
if (cancelRequested || settled) return;
|
|
3932
4374
|
onEvent({
|
|
3933
4375
|
type: "stderr",
|
|
3934
4376
|
data: err instanceof Error ? err.message : String(err)
|
|
3935
4377
|
});
|
|
3936
|
-
|
|
3937
|
-
|
|
4378
|
+
settle({
|
|
4379
|
+
reason: "startup-failed",
|
|
3938
4380
|
exitCode: null
|
|
3939
4381
|
});
|
|
3940
4382
|
return;
|
|
3941
4383
|
}
|
|
4384
|
+
if (cancelRequested || settled) return;
|
|
4385
|
+
activeCommand = fullCommand.join(" ");
|
|
3942
4386
|
onEvent({
|
|
3943
4387
|
type: "command",
|
|
3944
|
-
data:
|
|
4388
|
+
data: activeCommand
|
|
3945
4389
|
});
|
|
3946
4390
|
const [cmd, ...cmdArgs] = fullCommand;
|
|
3947
4391
|
const started = spawnSafe(cmd, cmdArgs, {
|
|
@@ -3951,8 +4395,8 @@ var CliExecutor = class {
|
|
|
3951
4395
|
});
|
|
3952
4396
|
if (!started.ok) {
|
|
3953
4397
|
const { code, message } = started.error;
|
|
3954
|
-
if (allowRetry && code === "ENOENT" && !
|
|
3955
|
-
|
|
4398
|
+
if (allowRetry && code === "ENOENT" && !cancelRequested && retryRunner) {
|
|
4399
|
+
retryRunner();
|
|
3956
4400
|
start(false);
|
|
3957
4401
|
return;
|
|
3958
4402
|
}
|
|
@@ -3960,14 +4404,18 @@ var CliExecutor = class {
|
|
|
3960
4404
|
type: "stderr",
|
|
3961
4405
|
data: message
|
|
3962
4406
|
});
|
|
3963
|
-
|
|
3964
|
-
|
|
4407
|
+
settle({
|
|
4408
|
+
reason: "startup-failed",
|
|
3965
4409
|
exitCode: null
|
|
3966
4410
|
});
|
|
3967
4411
|
return;
|
|
3968
4412
|
}
|
|
3969
4413
|
const child = started.child;
|
|
3970
|
-
|
|
4414
|
+
childOwner.claim(child);
|
|
4415
|
+
let childSpawned = false;
|
|
4416
|
+
child.once("spawn", () => {
|
|
4417
|
+
childSpawned = true;
|
|
4418
|
+
});
|
|
3971
4419
|
child.stdout?.on("data", (data) => {
|
|
3972
4420
|
onEvent({
|
|
3973
4421
|
type: "stdout",
|
|
@@ -3981,19 +4429,26 @@ var CliExecutor = class {
|
|
|
3981
4429
|
});
|
|
3982
4430
|
});
|
|
3983
4431
|
child.on("close", (exitCode) => {
|
|
3984
|
-
if (
|
|
3985
|
-
|
|
3986
|
-
|
|
3987
|
-
|
|
4432
|
+
if (!childOwner.owns(child)) return;
|
|
4433
|
+
childOwner.release(child);
|
|
4434
|
+
settle({
|
|
4435
|
+
reason: cancelRequested ? "cancelled" : "exited",
|
|
3988
4436
|
exitCode
|
|
3989
4437
|
});
|
|
3990
4438
|
});
|
|
3991
4439
|
child.on("error", (err) => {
|
|
3992
|
-
if (
|
|
3993
|
-
activeChild = null;
|
|
4440
|
+
if (!childOwner.owns(child)) return;
|
|
3994
4441
|
const { code, message } = formatSpawnError(err);
|
|
3995
|
-
if (
|
|
3996
|
-
|
|
4442
|
+
if (childSpawned) {
|
|
4443
|
+
onEvent({
|
|
4444
|
+
type: "stderr",
|
|
4445
|
+
data: message
|
|
4446
|
+
});
|
|
4447
|
+
return;
|
|
4448
|
+
}
|
|
4449
|
+
childOwner.release(child);
|
|
4450
|
+
if (allowRetry && code === "ENOENT" && !cancelRequested && retryRunner) {
|
|
4451
|
+
retryRunner();
|
|
3997
4452
|
start(false);
|
|
3998
4453
|
return;
|
|
3999
4454
|
}
|
|
@@ -4001,18 +4456,29 @@ var CliExecutor = class {
|
|
|
4001
4456
|
type: "stderr",
|
|
4002
4457
|
data: message
|
|
4003
4458
|
});
|
|
4004
|
-
|
|
4005
|
-
|
|
4459
|
+
settle({
|
|
4460
|
+
reason: cancelRequested ? "cancelled" : "startup-failed",
|
|
4006
4461
|
exitCode: null
|
|
4007
4462
|
});
|
|
4008
4463
|
});
|
|
4009
4464
|
};
|
|
4010
|
-
|
|
4011
|
-
|
|
4012
|
-
|
|
4013
|
-
|
|
4014
|
-
|
|
4465
|
+
start(true);
|
|
4466
|
+
const handle = {
|
|
4467
|
+
settled: settlement.promise,
|
|
4468
|
+
cancel: () => {
|
|
4469
|
+
if (cancelRequested || settled) return settlement.promise;
|
|
4470
|
+
cancelRequested = true;
|
|
4471
|
+
const child = childOwner.currentChild;
|
|
4472
|
+
if (child) requestChildTermination(child);
|
|
4473
|
+
else settle({
|
|
4474
|
+
reason: "cancelled",
|
|
4475
|
+
exitCode: null
|
|
4476
|
+
});
|
|
4477
|
+
return settlement.promise;
|
|
4478
|
+
}
|
|
4015
4479
|
};
|
|
4480
|
+
settlement.promise.catch(() => {});
|
|
4481
|
+
return handle;
|
|
4016
4482
|
}
|
|
4017
4483
|
/**
|
|
4018
4484
|
* 流式执行 openspec init
|
|
@@ -4038,6 +4504,7 @@ var CliExecutor = class {
|
|
|
4038
4504
|
];
|
|
4039
4505
|
if (options.skipSpecs) args.push("--skip-specs");
|
|
4040
4506
|
if (options.noValidate) args.push("--no-validate");
|
|
4507
|
+
if (options.store !== void 0) args.push("--store", options.store);
|
|
4041
4508
|
return this.executeStream(args, onEvent);
|
|
4042
4509
|
}
|
|
4043
4510
|
/**
|
|
@@ -4048,89 +4515,421 @@ var CliExecutor = class {
|
|
|
4048
4515
|
*/
|
|
4049
4516
|
executeCommandStream(command, onEvent) {
|
|
4050
4517
|
const [cmd, ...cmdArgs] = command;
|
|
4051
|
-
if (cmd === "openspec")
|
|
4052
|
-
|
|
4053
|
-
|
|
4054
|
-
|
|
4055
|
-
|
|
4056
|
-
|
|
4057
|
-
|
|
4058
|
-
|
|
4059
|
-
|
|
4060
|
-
|
|
4061
|
-
|
|
4062
|
-
|
|
4063
|
-
|
|
4064
|
-
|
|
4065
|
-
|
|
4066
|
-
|
|
4067
|
-
|
|
4068
|
-
|
|
4069
|
-
|
|
4070
|
-
|
|
4071
|
-
|
|
4072
|
-
|
|
4073
|
-
|
|
4518
|
+
if (cmd === "openspec") return this.executeStream([...cmdArgs], onEvent);
|
|
4519
|
+
return this.createStreamHandle(() => [cmd, ...cmdArgs], onEvent);
|
|
4520
|
+
}
|
|
4521
|
+
};
|
|
4522
|
+
|
|
4523
|
+
//#endregion
|
|
4524
|
+
//#region src/runtime-invalidation.ts
|
|
4525
|
+
/**
|
|
4526
|
+
* Orthogonal intents (created 2026-07-16 Asia/Shanghai):
|
|
4527
|
+
* 1. Define runtime invalidation facet identity without carrying projected domain data.
|
|
4528
|
+
* 2. Expose monotonic generations as ReactiveContext dependencies.
|
|
4529
|
+
* 3. Coalesce direct-listener pushes per facet while generations advance synchronously.
|
|
4530
|
+
*
|
|
4531
|
+
* Original request (2026-07-15): "Push 通知变更,然后让多端基于订阅拉取更新。"
|
|
4532
|
+
*/
|
|
4533
|
+
/** Complete runtime projection facet vocabulary. */
|
|
4534
|
+
const RUNTIME_INVALIDATION_FACETS = [
|
|
4535
|
+
"project",
|
|
4536
|
+
"stores",
|
|
4537
|
+
"worksets",
|
|
4538
|
+
"schemas",
|
|
4539
|
+
"context"
|
|
4540
|
+
];
|
|
4541
|
+
/** Environment-local invalidation identity. Authoritative data stays in CLI-backed projections. */
|
|
4542
|
+
var RuntimeInvalidationIndex = class {
|
|
4543
|
+
generations = /* @__PURE__ */ new Map();
|
|
4544
|
+
states = /* @__PURE__ */ new Map();
|
|
4545
|
+
listeners = /* @__PURE__ */ new Set();
|
|
4546
|
+
pendingNotifications = /* @__PURE__ */ new Map();
|
|
4547
|
+
notificationScheduled = false;
|
|
4548
|
+
constructor() {
|
|
4549
|
+
for (const facet of RUNTIME_INVALIDATION_FACETS) {
|
|
4550
|
+
this.generations.set(facet, 0);
|
|
4551
|
+
this.states.set(facet, new ReactiveState(0));
|
|
4074
4552
|
}
|
|
4075
|
-
|
|
4076
|
-
|
|
4077
|
-
|
|
4078
|
-
|
|
4079
|
-
|
|
4080
|
-
|
|
4081
|
-
|
|
4082
|
-
|
|
4553
|
+
}
|
|
4554
|
+
current(facet) {
|
|
4555
|
+
return this.generations.get(facet) ?? 0;
|
|
4556
|
+
}
|
|
4557
|
+
track(...facets) {
|
|
4558
|
+
return facets.map((facet) => ({
|
|
4559
|
+
facet,
|
|
4560
|
+
generation: this.states.get(facet)?.get() ?? 0
|
|
4561
|
+
}));
|
|
4562
|
+
}
|
|
4563
|
+
invalidate(facets) {
|
|
4564
|
+
const tokens = [...new Set(facets)].map((facet) => {
|
|
4565
|
+
const generation = this.current(facet) + 1;
|
|
4566
|
+
this.generations.set(facet, generation);
|
|
4567
|
+
this.states.get(facet)?.set(generation);
|
|
4568
|
+
return {
|
|
4569
|
+
facet,
|
|
4570
|
+
generation
|
|
4571
|
+
};
|
|
4083
4572
|
});
|
|
4084
|
-
|
|
4085
|
-
|
|
4086
|
-
|
|
4087
|
-
|
|
4088
|
-
|
|
4089
|
-
|
|
4090
|
-
|
|
4091
|
-
|
|
4092
|
-
|
|
4093
|
-
|
|
4573
|
+
this.enqueueNotifications(tokens);
|
|
4574
|
+
return tokens;
|
|
4575
|
+
}
|
|
4576
|
+
enqueueNotifications(tokens) {
|
|
4577
|
+
for (const record of this.listeners) {
|
|
4578
|
+
let pending = this.pendingNotifications.get(record);
|
|
4579
|
+
for (const token of tokens) {
|
|
4580
|
+
if (!record.facets.has(token.facet)) continue;
|
|
4581
|
+
pending ??= /* @__PURE__ */ new Map();
|
|
4582
|
+
pending.set(token.facet, token);
|
|
4583
|
+
}
|
|
4584
|
+
if (pending) this.pendingNotifications.set(record, pending);
|
|
4094
4585
|
}
|
|
4095
|
-
|
|
4096
|
-
|
|
4097
|
-
|
|
4098
|
-
|
|
4099
|
-
|
|
4100
|
-
|
|
4101
|
-
|
|
4102
|
-
|
|
4103
|
-
|
|
4104
|
-
|
|
4105
|
-
|
|
4586
|
+
if (this.pendingNotifications.size === 0 || this.notificationScheduled) return;
|
|
4587
|
+
this.notificationScheduled = true;
|
|
4588
|
+
queueMicrotask(() => this.flushNotifications());
|
|
4589
|
+
}
|
|
4590
|
+
flushNotifications() {
|
|
4591
|
+
const notifications = this.pendingNotifications;
|
|
4592
|
+
this.pendingNotifications = /* @__PURE__ */ new Map();
|
|
4593
|
+
this.notificationScheduled = false;
|
|
4594
|
+
for (const [record, pending] of notifications) {
|
|
4595
|
+
if (!this.listeners.has(record)) continue;
|
|
4596
|
+
const tokens = RUNTIME_INVALIDATION_FACETS.flatMap((facet) => {
|
|
4597
|
+
const token = pending.get(facet);
|
|
4598
|
+
return token ? [token] : [];
|
|
4106
4599
|
});
|
|
4107
|
-
|
|
4108
|
-
|
|
4109
|
-
|
|
4110
|
-
|
|
4111
|
-
|
|
4600
|
+
if (tokens.length === 0) continue;
|
|
4601
|
+
try {
|
|
4602
|
+
record.listener(tokens);
|
|
4603
|
+
} catch (error) {
|
|
4604
|
+
console.error("Runtime invalidation listener failed:", error);
|
|
4605
|
+
}
|
|
4606
|
+
}
|
|
4607
|
+
}
|
|
4608
|
+
subscribe(facets, listener) {
|
|
4609
|
+
const record = {
|
|
4610
|
+
facets: new Set(facets),
|
|
4611
|
+
listener
|
|
4612
|
+
};
|
|
4613
|
+
this.listeners.add(record);
|
|
4614
|
+
return () => {
|
|
4615
|
+
this.listeners.delete(record);
|
|
4616
|
+
this.pendingNotifications.delete(record);
|
|
4617
|
+
};
|
|
4618
|
+
}
|
|
4619
|
+
};
|
|
4620
|
+
|
|
4621
|
+
//#endregion
|
|
4622
|
+
//#region src/open-spec-data-home-observer.ts
|
|
4623
|
+
/**
|
|
4624
|
+
* Orthogonal intents (updated 2026-07-28 Asia/Shanghai):
|
|
4625
|
+
* 1. Own the effective OpenSpec data-home observation lease for one runtime environment.
|
|
4626
|
+
* 2. Map registry, Workset, and schema paths to their objective invalidation facets.
|
|
4627
|
+
* 3. Keep Store, Workset, schema, and Context projections responsible for fresh pulls.
|
|
4628
|
+
* 4. Release the path subscription and root lease deterministically.
|
|
4629
|
+
* 5. Settle initially missing official targets from bounded ancestor creation without invalidating unrelated content.
|
|
4630
|
+
*
|
|
4631
|
+
* Original request (2026-07-15): "有效 OpenSpec data home 的变化要让所有端拉取最新投影。"
|
|
4632
|
+
* Original request (2026-07-26): "真正基于文件、甚至是文件内容结构的变更去拉取更新。"
|
|
4633
|
+
* Remote CI fixed point (2026-07-28): data-home Schema creation may arrive as an ancestor event on Linux.
|
|
4634
|
+
*/
|
|
4635
|
+
/** Official OpenSpec 1.6 data-home paths and the projections whose inputs they can change. */
|
|
4636
|
+
const OPEN_SPEC_DATA_HOME_OBSERVATION_TARGETS = [
|
|
4637
|
+
{
|
|
4638
|
+
relativePath: "stores/registry.yaml",
|
|
4639
|
+
recursive: false,
|
|
4640
|
+
facets: ["stores", "context"]
|
|
4641
|
+
},
|
|
4642
|
+
{
|
|
4643
|
+
relativePath: "worksets",
|
|
4644
|
+
recursive: true,
|
|
4645
|
+
facets: ["worksets"]
|
|
4646
|
+
},
|
|
4647
|
+
{
|
|
4648
|
+
relativePath: "schemas",
|
|
4649
|
+
recursive: true,
|
|
4650
|
+
facets: ["schemas", "context"]
|
|
4651
|
+
}
|
|
4652
|
+
];
|
|
4653
|
+
/** Observe one effective OpenSpec data home without parsing or owning its domain data. */
|
|
4654
|
+
var OpenSpecDataHomeObserver = class {
|
|
4655
|
+
dataHomePath;
|
|
4656
|
+
environment;
|
|
4657
|
+
invalidation;
|
|
4658
|
+
startPromise = null;
|
|
4659
|
+
releasePathSubscriptions = [];
|
|
4660
|
+
releaseRoot = null;
|
|
4661
|
+
disposed = false;
|
|
4662
|
+
state = "idle";
|
|
4663
|
+
constructor(options) {
|
|
4664
|
+
this.dataHomePath = options.dataHomePath;
|
|
4665
|
+
this.environment = options.environment;
|
|
4666
|
+
this.invalidation = options.invalidation;
|
|
4667
|
+
}
|
|
4668
|
+
start() {
|
|
4669
|
+
if (this.disposed) return Promise.reject(/* @__PURE__ */ new Error("Cannot start data-home observation after teardown."));
|
|
4670
|
+
if (this.startPromise) return this.startPromise;
|
|
4671
|
+
this.state = "starting";
|
|
4672
|
+
this.releasePathSubscriptions = OPEN_SPEC_DATA_HOME_OBSERVATION_TARGETS.map((target) => {
|
|
4673
|
+
const targetPath = join$1(this.dataHomePath, target.relativePath);
|
|
4674
|
+
let wasPresent = existsSync(targetPath);
|
|
4675
|
+
return acquireWatcher(targetPath, () => {
|
|
4676
|
+
const isPresent = existsSync(targetPath);
|
|
4677
|
+
if (wasPresent || isPresent) this.invalidation.invalidate(target.facets);
|
|
4678
|
+
wasPresent = isPresent;
|
|
4679
|
+
}, {
|
|
4680
|
+
recursive: target.recursive,
|
|
4681
|
+
watchAncestorsWhileMissing: true
|
|
4112
4682
|
});
|
|
4113
4683
|
});
|
|
4114
|
-
|
|
4115
|
-
|
|
4116
|
-
|
|
4117
|
-
|
|
4118
|
-
|
|
4119
|
-
|
|
4120
|
-
|
|
4121
|
-
|
|
4122
|
-
|
|
4123
|
-
|
|
4684
|
+
const startPromise = this.environment.acquireRoot(this.dataHomePath).then(async (releaseRoot) => {
|
|
4685
|
+
if (this.disposed) {
|
|
4686
|
+
await releaseRoot();
|
|
4687
|
+
return;
|
|
4688
|
+
}
|
|
4689
|
+
this.releaseRoot = releaseRoot;
|
|
4690
|
+
this.state = "active";
|
|
4691
|
+
}).catch((error) => {
|
|
4692
|
+
this.releasePathSubscriptions.forEach((release) => release());
|
|
4693
|
+
this.releasePathSubscriptions = [];
|
|
4694
|
+
if (!this.disposed) this.state = "failed";
|
|
4695
|
+
if (this.startPromise === startPromise) this.startPromise = null;
|
|
4696
|
+
throw error;
|
|
4124
4697
|
});
|
|
4698
|
+
this.startPromise = startPromise;
|
|
4699
|
+
return this.startPromise;
|
|
4700
|
+
}
|
|
4701
|
+
/** Current lease state used by the bounded watcher-failure fallback. */
|
|
4702
|
+
getState() {
|
|
4703
|
+
return this.state;
|
|
4704
|
+
}
|
|
4705
|
+
async dispose() {
|
|
4706
|
+
if (this.disposed) return;
|
|
4707
|
+
this.disposed = true;
|
|
4708
|
+
this.state = "disposed";
|
|
4709
|
+
this.releasePathSubscriptions.forEach((release) => release());
|
|
4710
|
+
this.releasePathSubscriptions = [];
|
|
4711
|
+
await this.startPromise?.catch(() => {});
|
|
4712
|
+
const releaseRoot = this.releaseRoot;
|
|
4713
|
+
this.releaseRoot = null;
|
|
4714
|
+
await releaseRoot?.();
|
|
4715
|
+
}
|
|
4716
|
+
};
|
|
4717
|
+
|
|
4718
|
+
//#endregion
|
|
4719
|
+
//#region src/open-spec-data-scope.ts
|
|
4720
|
+
/**
|
|
4721
|
+
* Orthogonal intents (created 2026-07-16 Asia/Shanghai):
|
|
4722
|
+
* 1. Project the inherited OpenSpec user-data root without introducing project overrides.
|
|
4723
|
+
* 2. Match OpenSpec 1.6 XDG and platform fallback rules exactly.
|
|
4724
|
+
* 3. Preserve the environment source as read-only diagnostic evidence.
|
|
4725
|
+
*
|
|
4726
|
+
* Original request (2026-07-15): "OpenSpecUI inherits the launch environment's XDG_DATA_HOME."
|
|
4727
|
+
*/
|
|
4728
|
+
/** Directory name appended to the selected platform user-data root. */
|
|
4729
|
+
const OPEN_SPEC_DATA_DIRECTORY_NAME = "openspec";
|
|
4730
|
+
function joinPlatformPath(platform$1, ...segments) {
|
|
4731
|
+
return platform$1 === "win32" ? win32.join(...segments) : posix.join(...segments);
|
|
4732
|
+
}
|
|
4733
|
+
/** Resolve the read-only data-scope diagnostic using OpenSpec 1.6's platform rules. */
|
|
4734
|
+
function resolveOpenSpecDataScope(options = {}) {
|
|
4735
|
+
const env = options.env ?? process.env;
|
|
4736
|
+
const platform$1 = options.platform ?? platform();
|
|
4737
|
+
const homedir$1 = options.homedir ?? homedir();
|
|
4738
|
+
if (env.XDG_DATA_HOME) return {
|
|
4739
|
+
path: joinPlatformPath(platform$1, env.XDG_DATA_HOME, OPEN_SPEC_DATA_DIRECTORY_NAME),
|
|
4740
|
+
source: "xdg-data-home",
|
|
4741
|
+
environmentVariable: "XDG_DATA_HOME"
|
|
4742
|
+
};
|
|
4743
|
+
if (platform$1 === "win32" && env.LOCALAPPDATA) return {
|
|
4744
|
+
path: joinPlatformPath(platform$1, env.LOCALAPPDATA, OPEN_SPEC_DATA_DIRECTORY_NAME),
|
|
4745
|
+
source: "local-app-data",
|
|
4746
|
+
environmentVariable: "LOCALAPPDATA"
|
|
4747
|
+
};
|
|
4748
|
+
return {
|
|
4749
|
+
path: platform$1 === "win32" ? joinPlatformPath(platform$1, homedir$1, "AppData", "Local", OPEN_SPEC_DATA_DIRECTORY_NAME) : joinPlatformPath(platform$1, homedir$1, ".local", "share", OPEN_SPEC_DATA_DIRECTORY_NAME),
|
|
4750
|
+
source: "user-home-default",
|
|
4751
|
+
environmentVariable: null
|
|
4752
|
+
};
|
|
4753
|
+
}
|
|
4754
|
+
|
|
4755
|
+
//#endregion
|
|
4756
|
+
//#region src/root-context.ts
|
|
4757
|
+
/** Derive the only CLI selector that preserves an already resolved Root Context. */
|
|
4758
|
+
function getRootContextCliSelector(rootContext) {
|
|
4759
|
+
const planningRoot = rootContext.planningRoot;
|
|
4760
|
+
if (!planningRoot) throw new Error("Cannot derive a CLI selector without a resolved planning root.");
|
|
4761
|
+
if (planningRoot.source !== "store") return {};
|
|
4762
|
+
if (!rootContext.storeId) throw new Error("OpenSpec reported an explicit Store root without a Store id.");
|
|
4763
|
+
return { store: rootContext.storeId };
|
|
4764
|
+
}
|
|
4765
|
+
function commandMessage(evidence, fallback) {
|
|
4766
|
+
return evidence.contractError || evidence.stderr.trim() || evidence.diagnostics.map((diagnostic) => diagnostic.message).join("\n") || fallback;
|
|
4767
|
+
}
|
|
4768
|
+
function publicCommandEvidence(evidence) {
|
|
4769
|
+
if (!evidence) return null;
|
|
4770
|
+
return {
|
|
4771
|
+
success: evidence.success,
|
|
4772
|
+
stdout: evidence.stdout,
|
|
4773
|
+
stderr: evidence.stderr,
|
|
4774
|
+
exitCode: evidence.exitCode,
|
|
4775
|
+
diagnostics: evidence.diagnostics,
|
|
4776
|
+
...evidence.contractError ? { contractError: evidence.contractError } : {}
|
|
4777
|
+
};
|
|
4778
|
+
}
|
|
4779
|
+
function createRootContext(options, cli, doctor, context, observedAt) {
|
|
4780
|
+
const doctorData = doctor?.data ?? null;
|
|
4781
|
+
const contextData = context?.data ?? null;
|
|
4782
|
+
const planningRoot = doctorData?.root ?? null;
|
|
4783
|
+
return {
|
|
4784
|
+
launchProject: { path: options.launchProjectDir },
|
|
4785
|
+
planningRoot,
|
|
4786
|
+
storeId: planningRoot?.store_id ?? doctorData?.store?.id ?? null,
|
|
4787
|
+
cli,
|
|
4788
|
+
references: doctorData?.references ?? [],
|
|
4789
|
+
contextMembers: contextData?.members ?? [],
|
|
4790
|
+
dataScope: resolveOpenSpecDataScope(options),
|
|
4791
|
+
diagnostics: {
|
|
4792
|
+
root: planningRoot?.status ?? [],
|
|
4793
|
+
doctor: doctorData?.status ?? doctor?.diagnostics ?? [],
|
|
4794
|
+
context: contextData?.status ?? context?.diagnostics ?? []
|
|
4795
|
+
},
|
|
4796
|
+
evidence: {
|
|
4797
|
+
doctor: publicCommandEvidence(doctor),
|
|
4798
|
+
context: publicCommandEvidence(context)
|
|
4799
|
+
},
|
|
4800
|
+
observedAt
|
|
4801
|
+
};
|
|
4802
|
+
}
|
|
4803
|
+
function errorState(attempt, code, message) {
|
|
4804
|
+
return {
|
|
4805
|
+
state: "error",
|
|
4806
|
+
data: null,
|
|
4807
|
+
attempt,
|
|
4808
|
+
error: {
|
|
4809
|
+
code,
|
|
4810
|
+
message
|
|
4811
|
+
},
|
|
4812
|
+
observedAt: attempt.observedAt
|
|
4813
|
+
};
|
|
4814
|
+
}
|
|
4815
|
+
function rootsMatch(doctor, context) {
|
|
4816
|
+
return doctor.path === context.path && doctor.source === context.source && (doctor.store_id === void 0 || context.store_id === void 0 || doctor.store_id === context.store_id);
|
|
4817
|
+
}
|
|
4818
|
+
function classifyRootContext(attempt, doctor, context) {
|
|
4819
|
+
const { cli, planningRoot, references } = attempt;
|
|
4820
|
+
if (!cli.available) return errorState(attempt, "cli-unavailable", cli.error ?? "OpenSpec CLI is unavailable.");
|
|
4821
|
+
if (!doctor) return errorState(attempt, "resolver-failed", "OpenSpec Doctor did not return command evidence.");
|
|
4822
|
+
if (doctor.contractError) return errorState(attempt, "doctor-contract-drift", commandMessage(doctor, "OpenSpec Doctor returned an incompatible payload."));
|
|
4823
|
+
if (!doctor.success) return errorState(attempt, "doctor-command-failed", commandMessage(doctor, "OpenSpec Doctor failed."));
|
|
4824
|
+
if (!planningRoot) return errorState(attempt, "root-unresolved", "OpenSpec Doctor did not resolve a planning root.");
|
|
4825
|
+
if (!planningRoot.healthy) return errorState(attempt, "root-unhealthy", planningRoot.status.map((diagnostic) => diagnostic.message).join("\n") || "OpenSpec Doctor reported an unhealthy planning root.");
|
|
4826
|
+
if (!context) return errorState(attempt, "resolver-failed", "OpenSpec Context did not return command evidence.");
|
|
4827
|
+
if (context.contractError) return errorState(attempt, "context-contract-drift", commandMessage(context, "OpenSpec Context returned an incompatible payload."));
|
|
4828
|
+
if (!context.success) return errorState(attempt, "context-command-failed", commandMessage(context, "OpenSpec Context failed."));
|
|
4829
|
+
if (!context.data?.root || !rootsMatch(planningRoot, context.data.root)) return errorState(attempt, "context-root-mismatch", "OpenSpec Doctor and Context did not report the same planning root.");
|
|
4830
|
+
const failedReference = references.flatMap((reference) => reference.status).find((diagnostic) => diagnostic.severity === "error");
|
|
4831
|
+
if (failedReference) return errorState(attempt, "references-unresolved", failedReference.message);
|
|
4832
|
+
return {
|
|
4833
|
+
state: "ready",
|
|
4834
|
+
data: attempt,
|
|
4835
|
+
attempt: null,
|
|
4836
|
+
error: null,
|
|
4837
|
+
observedAt: attempt.observedAt
|
|
4838
|
+
};
|
|
4839
|
+
}
|
|
4840
|
+
function unavailableCli(error) {
|
|
4841
|
+
return {
|
|
4842
|
+
available: false,
|
|
4843
|
+
error: error instanceof Error ? error.message : String(error)
|
|
4844
|
+
};
|
|
4845
|
+
}
|
|
4846
|
+
/** Resolve one current Root Context attempt from official CLI projections and process scope. */
|
|
4847
|
+
async function resolveRootContext(options) {
|
|
4848
|
+
const observedAt = options.now?.() ?? Date.now();
|
|
4849
|
+
let cli;
|
|
4850
|
+
try {
|
|
4851
|
+
cli = await options.cliExecutor.checkAvailability();
|
|
4852
|
+
} catch (error) {
|
|
4853
|
+
cli = unavailableCli(error);
|
|
4854
|
+
}
|
|
4855
|
+
if (!cli.available) return classifyRootContext(createRootContext(options, cli, null, null, observedAt), null, null);
|
|
4856
|
+
const selector = {};
|
|
4857
|
+
const [doctorSettled, contextSettled] = await Promise.allSettled([options.cliExecutor.contracts.doctorRoot(selector), options.cliExecutor.contracts.context(selector)]);
|
|
4858
|
+
const doctor = doctorSettled.status === "fulfilled" ? doctorSettled.value : null;
|
|
4859
|
+
const context = contextSettled.status === "fulfilled" ? contextSettled.value : null;
|
|
4860
|
+
const attempt = createRootContext(options, cli, doctor, context, observedAt);
|
|
4861
|
+
if (doctorSettled.status === "rejected" || contextSettled.status === "rejected") {
|
|
4862
|
+
let failure;
|
|
4863
|
+
if (doctorSettled.status === "rejected") failure = doctorSettled.reason;
|
|
4864
|
+
else if (contextSettled.status === "rejected") failure = contextSettled.reason;
|
|
4865
|
+
return errorState(attempt, "resolver-failed", failure instanceof Error ? failure.message : String(failure));
|
|
4866
|
+
}
|
|
4867
|
+
return classifyRootContext(attempt, doctor, context);
|
|
4868
|
+
}
|
|
4869
|
+
|
|
4870
|
+
//#endregion
|
|
4871
|
+
//#region src/runtime-root-invalidation.ts
|
|
4872
|
+
/**
|
|
4873
|
+
* Orthogonal intents (created 2026-07-16 Asia/Shanghai):
|
|
4874
|
+
* 1. Map dynamic filesystem roots to selected runtime invalidation facets.
|
|
4875
|
+
* 2. Share one recursive path subscription for repeated normalized roots.
|
|
4876
|
+
* 3. Release root subscriptions after their final logical owner or registry teardown.
|
|
4877
|
+
*
|
|
4878
|
+
* Original request (2026-07-15): "Launch-project and connected planning-root changes invalidate their project/context facets."
|
|
4879
|
+
*/
|
|
4880
|
+
/** Reference-counted path-to-facet mapping; physical watcher roots remain separately owned. */
|
|
4881
|
+
var RuntimeRootInvalidationRegistry = class {
|
|
4882
|
+
records = /* @__PURE__ */ new Map();
|
|
4883
|
+
constructor(invalidation, facets) {
|
|
4884
|
+
this.invalidation = invalidation;
|
|
4885
|
+
this.facets = facets;
|
|
4886
|
+
}
|
|
4887
|
+
acquireRoot(rootPath) {
|
|
4888
|
+
const normalizedRoot = resolveRealPathThroughExistingAncestor(rootPath);
|
|
4889
|
+
let record = this.records.get(normalizedRoot);
|
|
4890
|
+
if (!record) {
|
|
4891
|
+
record = {
|
|
4892
|
+
rootPath: normalizedRoot,
|
|
4893
|
+
referenceCount: 0,
|
|
4894
|
+
releasePath: acquireWatcher(normalizedRoot, () => this.invalidation.invalidate(this.facets), { recursive: true })
|
|
4895
|
+
};
|
|
4896
|
+
this.records.set(normalizedRoot, record);
|
|
4897
|
+
}
|
|
4898
|
+
record.referenceCount += 1;
|
|
4899
|
+
let released = false;
|
|
4125
4900
|
return () => {
|
|
4126
|
-
|
|
4901
|
+
if (released) return;
|
|
4902
|
+
released = true;
|
|
4903
|
+
const current = this.records.get(normalizedRoot);
|
|
4904
|
+
if (current !== record || current.referenceCount === 0) return;
|
|
4905
|
+
current.referenceCount -= 1;
|
|
4906
|
+
if (current.referenceCount > 0) return;
|
|
4907
|
+
current.releasePath();
|
|
4908
|
+
this.records.delete(normalizedRoot);
|
|
4127
4909
|
};
|
|
4128
4910
|
}
|
|
4911
|
+
getRoots() {
|
|
4912
|
+
return [...this.records.values()].map(({ rootPath, referenceCount }) => ({
|
|
4913
|
+
rootPath,
|
|
4914
|
+
referenceCount
|
|
4915
|
+
})).sort((left, right) => left.rootPath.localeCompare(right.rootPath));
|
|
4916
|
+
}
|
|
4917
|
+
dispose() {
|
|
4918
|
+
const records = [...this.records.values()];
|
|
4919
|
+
this.records.clear();
|
|
4920
|
+
for (const record of records) record.releasePath();
|
|
4921
|
+
}
|
|
4129
4922
|
};
|
|
4130
4923
|
|
|
4131
4924
|
//#endregion
|
|
4132
4925
|
//#region src/tool-config.ts
|
|
4133
4926
|
/**
|
|
4927
|
+
* Orthogonal intents (updated 2026-07-15 Asia/Shanghai):
|
|
4928
|
+
* 1. Mirror official OpenSpec AI tool metadata and generated skill names.
|
|
4929
|
+
* 2. Detect configured project-local AI tool roots reactively.
|
|
4930
|
+
*
|
|
4931
|
+
* Original request (2026-07-15): "Oh My Pi、Trae 的完整交付链。"
|
|
4932
|
+
*
|
|
4134
4933
|
* 工具配置检测模块
|
|
4135
4934
|
*
|
|
4136
4935
|
* 对齐 @fission-ai/openspec 的 skills 体系,
|
|
@@ -4145,6 +4944,7 @@ const SKILL_NAMES = [
|
|
|
4145
4944
|
"openspec-new-change",
|
|
4146
4945
|
"openspec-continue-change",
|
|
4147
4946
|
"openspec-apply-change",
|
|
4947
|
+
"openspec-update-change",
|
|
4148
4948
|
"openspec-ff-change",
|
|
4149
4949
|
"openspec-sync-specs",
|
|
4150
4950
|
"openspec-archive-change",
|
|
@@ -4314,6 +5114,27 @@ const AI_TOOLS = [
|
|
|
4314
5114
|
successLabel: "Kiro",
|
|
4315
5115
|
skillsDir: ".kiro"
|
|
4316
5116
|
},
|
|
5117
|
+
{
|
|
5118
|
+
name: "Lingma",
|
|
5119
|
+
value: "lingma",
|
|
5120
|
+
available: true,
|
|
5121
|
+
successLabel: "Lingma",
|
|
5122
|
+
skillsDir: ".lingma"
|
|
5123
|
+
},
|
|
5124
|
+
{
|
|
5125
|
+
name: "Mistral Vibe",
|
|
5126
|
+
value: "vibe",
|
|
5127
|
+
available: true,
|
|
5128
|
+
successLabel: "Mistral Vibe",
|
|
5129
|
+
skillsDir: ".vibe"
|
|
5130
|
+
},
|
|
5131
|
+
{
|
|
5132
|
+
name: "Oh My Pi",
|
|
5133
|
+
value: "oh-my-pi",
|
|
5134
|
+
available: true,
|
|
5135
|
+
successLabel: "Oh My Pi",
|
|
5136
|
+
skillsDir: ".omp"
|
|
5137
|
+
},
|
|
4317
5138
|
{
|
|
4318
5139
|
name: "OpenCode",
|
|
4319
5140
|
value: "opencode",
|
|
@@ -4335,20 +5156,6 @@ const AI_TOOLS = [
|
|
|
4335
5156
|
successLabel: "Qoder",
|
|
4336
5157
|
skillsDir: ".qoder"
|
|
4337
5158
|
},
|
|
4338
|
-
{
|
|
4339
|
-
name: "Lingma",
|
|
4340
|
-
value: "lingma",
|
|
4341
|
-
available: true,
|
|
4342
|
-
successLabel: "Lingma",
|
|
4343
|
-
skillsDir: ".lingma"
|
|
4344
|
-
},
|
|
4345
|
-
{
|
|
4346
|
-
name: "Mistral Vibe",
|
|
4347
|
-
value: "vibe",
|
|
4348
|
-
available: true,
|
|
4349
|
-
successLabel: "Mistral Vibe",
|
|
4350
|
-
skillsDir: ".vibe"
|
|
4351
|
-
},
|
|
4352
5159
|
{
|
|
4353
5160
|
name: "Qwen Code",
|
|
4354
5161
|
value: "qwen",
|
|
@@ -4491,22 +5298,23 @@ async function isToolConfigured(projectDir, toolId) {
|
|
|
4491
5298
|
|
|
4492
5299
|
//#endregion
|
|
4493
5300
|
//#region src/tool-init-state.ts
|
|
4494
|
-
|
|
4495
|
-
|
|
4496
|
-
|
|
4497
|
-
|
|
4498
|
-
|
|
4499
|
-
|
|
4500
|
-
|
|
4501
|
-
|
|
4502
|
-
|
|
4503
|
-
|
|
4504
|
-
|
|
4505
|
-
|
|
4506
|
-
|
|
4507
|
-
|
|
5301
|
+
/**
|
|
5302
|
+
* Orthogonal intents (updated 2026-07-28 Asia/Shanghai):
|
|
5303
|
+
* 1. Map official OPSX workflows to generated skill and command locations.
|
|
5304
|
+
* 2. Project launch-local skills and physically scoped tool command initialization state.
|
|
5305
|
+
* 3. Provide the runtime owner with the same external Codex command root used by command projection.
|
|
5306
|
+
* 4. Bound reactive observation through existing parent inventories while preserving fresh one-shot reads.
|
|
5307
|
+
*
|
|
5308
|
+
* Original request (2026-07-15): "sync、update、Oh My Pi、Trae 的完整交付链。"
|
|
5309
|
+
* Remote CI fixed point (2026-07-28): Launch-local skill creation must converge when its tool root was absent at initial projection.
|
|
5310
|
+
*/
|
|
5311
|
+
/** Return the external runtime directory containing official Codex command prompts. */
|
|
5312
|
+
function getExternalCodexCommandObservationRoot() {
|
|
5313
|
+
return join$1(resolveCodexHome(), "prompts");
|
|
5314
|
+
}
|
|
5315
|
+
const ALL_TOOL_WORKFLOWS = [...OPSX_ALL_WORKFLOWS];
|
|
4508
5316
|
function toKnownWorkflows(workflows) {
|
|
4509
|
-
return workflows.filter((workflow) => workflow in
|
|
5317
|
+
return workflows.filter((workflow) => workflow in OPSX_WORKFLOW_TO_SKILL_DIR);
|
|
4510
5318
|
}
|
|
4511
5319
|
function resolveCodexHome() {
|
|
4512
5320
|
const configuredHome = process.env.CODEX_HOME?.trim();
|
|
@@ -4537,10 +5345,12 @@ const TOOL_COMMAND_PATHS = {
|
|
|
4537
5345
|
primary: (projectDir, workflow) => resolve(projectDir, ".opencode", "commands", `opsx-${workflow}.md`),
|
|
4538
5346
|
legacy: [(projectDir, workflow) => resolve(projectDir, ".opencode", "command", `opsx-${workflow}.md`)]
|
|
4539
5347
|
},
|
|
5348
|
+
"oh-my-pi": { primary: (projectDir, workflow) => resolve(projectDir, ".omp", "commands", `opsx-${workflow}.md`) },
|
|
4540
5349
|
pi: { primary: (projectDir, workflow) => resolve(projectDir, ".pi", "prompts", `opsx-${workflow}.md`) },
|
|
4541
5350
|
qoder: { primary: (projectDir, workflow) => resolve(projectDir, ".qoder", "commands", "opsx", `${workflow}.md`) },
|
|
4542
5351
|
qwen: { primary: (projectDir, workflow) => resolve(projectDir, ".qwen", "commands", `opsx-${workflow}.toml`) },
|
|
4543
5352
|
roocode: { primary: (projectDir, workflow) => resolve(projectDir, ".roo", "commands", `opsx-${workflow}.md`) },
|
|
5353
|
+
trae: { primary: (projectDir, workflow) => resolve(projectDir, ".trae", "commands", `opsx-${workflow}.md`) },
|
|
4544
5354
|
windsurf: { primary: (projectDir, workflow) => resolve(projectDir, ".windsurf", "workflows", `opsx-${workflow}.md`) }
|
|
4545
5355
|
};
|
|
4546
5356
|
function resolveToolCommandArtifact(projectDir, toolId, workflow) {
|
|
@@ -4555,7 +5365,7 @@ function resolveToolCommandArtifact(projectDir, toolId, workflow) {
|
|
|
4555
5365
|
function getSkillArtifacts(projectDir, skillsDir) {
|
|
4556
5366
|
return ALL_TOOL_WORKFLOWS.map((workflow) => ({
|
|
4557
5367
|
workflow,
|
|
4558
|
-
path: resolve(projectDir, skillsDir, "skills",
|
|
5368
|
+
path: resolve(projectDir, skillsDir, "skills", OPSX_WORKFLOW_TO_SKILL_DIR[workflow], "SKILL.md")
|
|
4559
5369
|
}));
|
|
4560
5370
|
}
|
|
4561
5371
|
function getCommandArtifacts(projectDir, toolId) {
|
|
@@ -4565,7 +5375,7 @@ function getCommandArtifacts(projectDir, toolId) {
|
|
|
4565
5375
|
});
|
|
4566
5376
|
}
|
|
4567
5377
|
function invalidateToolInitCaches(projectDir) {
|
|
4568
|
-
const cacheRoots =
|
|
5378
|
+
const cacheRoots = new Set([resolve(projectDir)]);
|
|
4569
5379
|
for (const tool of AI_TOOLS) {
|
|
4570
5380
|
if (tool.skillsDir) cacheRoots.add(resolve(projectDir, tool.skillsDir));
|
|
4571
5381
|
for (const commandArtifact of getCommandArtifacts(projectDir, tool.value)) {
|
|
@@ -4575,13 +5385,34 @@ function invalidateToolInitCaches(projectDir) {
|
|
|
4575
5385
|
}
|
|
4576
5386
|
for (const root of cacheRoots) clearCache(root);
|
|
4577
5387
|
}
|
|
4578
|
-
async function
|
|
4579
|
-
|
|
4580
|
-
|
|
4581
|
-
|
|
4582
|
-
|
|
5388
|
+
async function readArtifactDirectory(dir) {
|
|
5389
|
+
return new Set(await reactiveReadDir(dir, { includeHidden: true }));
|
|
5390
|
+
}
|
|
5391
|
+
async function getExistingSkillPaths(projectDir, skillsDir, entries, projectRootEntries) {
|
|
5392
|
+
const toolRoot = resolve(projectDir, skillsDir);
|
|
5393
|
+
if (!projectRootEntries.has(basename(toolRoot))) return /* @__PURE__ */ new Set();
|
|
5394
|
+
if (!(await readArtifactDirectory(toolRoot)).has("skills")) return /* @__PURE__ */ new Set();
|
|
5395
|
+
const rootEntries = await readArtifactDirectory(resolve(toolRoot, "skills"));
|
|
5396
|
+
const presentEntries = entries.filter((entry) => rootEntries.has(basename(dirname$1(entry.path))));
|
|
5397
|
+
const directoryEntries = await Promise.all(presentEntries.map(async (entry) => ({
|
|
5398
|
+
entry,
|
|
5399
|
+
files: await readArtifactDirectory(dirname$1(entry.path))
|
|
5400
|
+
})));
|
|
5401
|
+
return new Set(directoryEntries.filter(({ entry, files }) => files.has(basename(entry.path))).map(({ entry }) => entry.path));
|
|
5402
|
+
}
|
|
5403
|
+
async function getExistingCommandPaths(entries) {
|
|
5404
|
+
const pathsByDirectory = /* @__PURE__ */ new Map();
|
|
5405
|
+
for (const entry of entries) for (const path of [entry.path, ...entry.legacyPaths ?? []]) {
|
|
5406
|
+
const directory = dirname$1(path);
|
|
5407
|
+
const paths = pathsByDirectory.get(directory) ?? [];
|
|
5408
|
+
paths.push(path);
|
|
5409
|
+
pathsByDirectory.set(directory, paths);
|
|
5410
|
+
}
|
|
5411
|
+
const directoryEntries = await Promise.all([...pathsByDirectory].map(async ([directory, paths]) => ({
|
|
5412
|
+
paths,
|
|
5413
|
+
files: await readArtifactDirectory(directory)
|
|
4583
5414
|
})));
|
|
4584
|
-
return new Set(
|
|
5415
|
+
return new Set(directoryEntries.flatMap(({ paths, files }) => paths.filter((path) => files.has(basename(path)))));
|
|
4585
5416
|
}
|
|
4586
5417
|
function hasExistingArtifact(entry, existingPaths) {
|
|
4587
5418
|
return existingPaths.has(entry.path) || (entry.legacyPaths?.some((legacyPath) => existingPaths.has(legacyPath)) ?? false);
|
|
@@ -4601,17 +5432,17 @@ function collectUnexpectedWorkflows(entries, desiredWorkflowSet, existingPaths)
|
|
|
4601
5432
|
function collectLegacyWorkflows(entries, existingPaths) {
|
|
4602
5433
|
return entries.filter((entry) => hasLegacyArtifact(entry, existingPaths)).map((entry) => entry.workflow);
|
|
4603
5434
|
}
|
|
4604
|
-
async function
|
|
4605
|
-
invalidateToolInitCaches(projectDir);
|
|
5435
|
+
async function projectToolInitStates(projectDir, options) {
|
|
4606
5436
|
const desiredWorkflows = toKnownWorkflows(options.workflows);
|
|
4607
5437
|
const desiredWorkflowSet = new Set(desiredWorkflows);
|
|
4608
5438
|
const shouldGenerateSkills = options.delivery !== "commands";
|
|
4609
5439
|
const shouldGenerateCommands = options.delivery !== "skills";
|
|
5440
|
+
const projectRootEntries = await readArtifactDirectory(projectDir);
|
|
4610
5441
|
return Promise.all(AI_TOOLS.filter((tool) => tool.skillsDir).map(async (tool) => {
|
|
4611
5442
|
const skillArtifacts = getSkillArtifacts(projectDir, tool.skillsDir);
|
|
4612
5443
|
const commandArtifacts = getCommandArtifacts(projectDir, tool.value);
|
|
4613
|
-
const existingSkillPaths = await
|
|
4614
|
-
const existingCommandPaths = await
|
|
5444
|
+
const existingSkillPaths = await getExistingSkillPaths(projectDir, tool.skillsDir, skillArtifacts, projectRootEntries);
|
|
5445
|
+
const existingCommandPaths = await getExistingCommandPaths(commandArtifacts);
|
|
4615
5446
|
const expectedSkillArtifacts = shouldGenerateSkills ? skillArtifacts.filter((entry) => desiredWorkflowSet.has(entry.workflow)) : [];
|
|
4616
5447
|
const expectedCommandArtifacts = shouldGenerateCommands ? commandArtifacts.filter((entry) => desiredWorkflowSet.has(entry.workflow)) : [];
|
|
4617
5448
|
const missingSkillWorkflows = collectMissingWorkflows(expectedSkillArtifacts, existingSkillPaths);
|
|
@@ -4646,165 +5477,167 @@ async function getToolInitStates(projectDir, options) {
|
|
|
4646
5477
|
};
|
|
4647
5478
|
}));
|
|
4648
5479
|
}
|
|
4649
|
-
|
|
4650
|
-
|
|
4651
|
-
|
|
4652
|
-
|
|
4653
|
-
|
|
4654
|
-
|
|
4655
|
-
|
|
4656
|
-
|
|
4657
|
-
|
|
4658
|
-
|
|
4659
|
-
|
|
5480
|
+
/** Read a fresh one-shot Project Tool artifact inventory. */
|
|
5481
|
+
async function getToolInitStates(projectDir, options) {
|
|
5482
|
+
invalidateToolInitCaches(projectDir);
|
|
5483
|
+
return projectToolInitStates(projectDir, options);
|
|
5484
|
+
}
|
|
5485
|
+
/** Create a retained Tool inventory task that must be executed by one ReactiveContext. */
|
|
5486
|
+
function createToolInitStateProjection(projectDir, options) {
|
|
5487
|
+
const projectionOptions = {
|
|
5488
|
+
delivery: options.delivery,
|
|
5489
|
+
workflows: [...options.workflows]
|
|
5490
|
+
};
|
|
5491
|
+
return () => projectToolInitStates(projectDir, projectionOptions);
|
|
5492
|
+
}
|
|
4660
5493
|
|
|
4661
5494
|
//#endregion
|
|
4662
5495
|
//#region src/hooks.ts
|
|
4663
|
-
const
|
|
5496
|
+
const OPENSPECUI_DOCUMENT_HOOK_VERSION = 1;
|
|
5497
|
+
const OPENSPECUI_WORKFLOW_HOOK_VERSION = 2;
|
|
4664
5498
|
|
|
4665
5499
|
//#endregion
|
|
4666
|
-
//#region src/
|
|
4667
|
-
/**
|
|
4668
|
-
|
|
4669
|
-
|
|
4670
|
-
|
|
4671
|
-
|
|
4672
|
-
|
|
4673
|
-
|
|
4674
|
-
|
|
4675
|
-
|
|
4676
|
-
|
|
4677
|
-
|
|
4678
|
-
|
|
4679
|
-
|
|
4680
|
-
|
|
4681
|
-
|
|
4682
|
-
|
|
4683
|
-
|
|
4684
|
-
|
|
4685
|
-
|
|
4686
|
-
|
|
4687
|
-
|
|
4688
|
-
|
|
4689
|
-
|
|
4690
|
-
|
|
4691
|
-
|
|
4692
|
-
|
|
4693
|
-
|
|
4694
|
-
|
|
4695
|
-
const
|
|
4696
|
-
|
|
4697
|
-
|
|
4698
|
-
|
|
4699
|
-
}
|
|
4700
|
-
|
|
4701
|
-
|
|
4702
|
-
|
|
4703
|
-
|
|
4704
|
-
|
|
4705
|
-
|
|
4706
|
-
|
|
4707
|
-
|
|
4708
|
-
|
|
4709
|
-
|
|
4710
|
-
|
|
4711
|
-
}
|
|
4712
|
-
|
|
4713
|
-
|
|
4714
|
-
|
|
4715
|
-
|
|
4716
|
-
|
|
4717
|
-
|
|
4718
|
-
|
|
4719
|
-
|
|
4720
|
-
|
|
4721
|
-
|
|
4722
|
-
|
|
4723
|
-
|
|
4724
|
-
|
|
4725
|
-
|
|
4726
|
-
|
|
4727
|
-
|
|
4728
|
-
|
|
4729
|
-
|
|
4730
|
-
|
|
4731
|
-
|
|
4732
|
-
|
|
4733
|
-
|
|
4734
|
-
|
|
4735
|
-
});
|
|
4736
|
-
const SchemaInfoSchema = z.object({
|
|
4737
|
-
name: z.string(),
|
|
4738
|
-
description: z.string().optional(),
|
|
4739
|
-
artifacts: z.array(z.string()),
|
|
4740
|
-
source: z.enum([
|
|
4741
|
-
"project",
|
|
4742
|
-
"user",
|
|
4743
|
-
"package"
|
|
4744
|
-
])
|
|
4745
|
-
});
|
|
4746
|
-
const SchemaResolutionSchema = z.object({
|
|
4747
|
-
name: z.string(),
|
|
4748
|
-
source: z.enum([
|
|
4749
|
-
"project",
|
|
4750
|
-
"user",
|
|
4751
|
-
"package"
|
|
4752
|
-
]),
|
|
4753
|
-
path: z.string(),
|
|
4754
|
-
displayPath: z.string().optional(),
|
|
4755
|
-
shadows: z.array(z.object({
|
|
4756
|
-
source: z.enum([
|
|
4757
|
-
"project",
|
|
4758
|
-
"user",
|
|
4759
|
-
"package"
|
|
4760
|
-
]),
|
|
4761
|
-
path: z.string(),
|
|
4762
|
-
displayPath: z.string().optional()
|
|
4763
|
-
}))
|
|
4764
|
-
});
|
|
4765
|
-
const TemplatesSchema = z.record(z.object({
|
|
4766
|
-
path: z.string(),
|
|
4767
|
-
displayPath: z.string().optional(),
|
|
4768
|
-
source: z.enum([
|
|
4769
|
-
"project",
|
|
4770
|
-
"user",
|
|
4771
|
-
"package"
|
|
4772
|
-
])
|
|
4773
|
-
}));
|
|
4774
|
-
const SchemaArtifactSchema = z.object({
|
|
4775
|
-
id: z.string(),
|
|
4776
|
-
outputPath: z.string(),
|
|
4777
|
-
description: z.string().optional(),
|
|
4778
|
-
template: z.string().optional(),
|
|
4779
|
-
instruction: z.string().optional(),
|
|
4780
|
-
requires: z.array(z.string())
|
|
4781
|
-
});
|
|
4782
|
-
const SchemaDetailSchema = z.object({
|
|
4783
|
-
name: z.string(),
|
|
4784
|
-
description: z.string().optional(),
|
|
4785
|
-
version: z.union([z.string(), z.number()]).optional(),
|
|
4786
|
-
artifacts: z.array(SchemaArtifactSchema),
|
|
4787
|
-
applyRequires: z.array(z.string()),
|
|
4788
|
-
applyTracks: z.string().optional(),
|
|
4789
|
-
applyInstruction: z.string().optional()
|
|
4790
|
-
});
|
|
5500
|
+
//#region src/hosted-protocol.ts
|
|
5501
|
+
/**
|
|
5502
|
+
* Orthogonal intents (updated 2026-07-25 Asia/Shanghai):
|
|
5503
|
+
* 1. Compute opaque environment identity and Access Gate credentials with Node crypto.
|
|
5504
|
+
* 2. Re-export browser-safe hosted identity/capability/mutation facts without making this Node entry browser-safe.
|
|
5505
|
+
*
|
|
5506
|
+
* Original request (2026-07-15): "我们可以在 cli 上新增一个 --auth 或者 --password。"
|
|
5507
|
+
* Migrated from `packages/app/src/types/{env-uri,capabilities,store-mutation}.ts` so the backend is the
|
|
5508
|
+
* authoritative producer and both App and Server share one typed contract.
|
|
5509
|
+
*
|
|
5510
|
+
* Key invariants (AGENTS.md):
|
|
5511
|
+
* - `envUri` is backend-issued, opaque, non-dereferenceable; it identifies host identity + effective
|
|
5512
|
+
* OpenSpec data home. Multiple processes/ports/projects share one envUri when that pair is unchanged.
|
|
5513
|
+
* - Capabilities are objective compatibility facts, never permissions, authorization, or workflow state.
|
|
5514
|
+
* - Store mutations are backend-owned: `accepted -> running -> succeeded | failed`; unrecoverable
|
|
5515
|
+
* terminal loss is `indeterminate`, never fabricated as failure or cancellation. V1 has no Cancel
|
|
5516
|
+
* and no automatic retry. A client request id deduplicates starts within one backend process.
|
|
5517
|
+
* - The Access Gate is one shared Bearer credential for the whole backend boundary; it is not an
|
|
5518
|
+
* account, role, ACL, or permission system, and it provides no transport confidentiality.
|
|
5519
|
+
*/
|
|
5520
|
+
/**
|
|
5521
|
+
* Compute a stable opaque `envUri` from backend host identity plus effective OpenSpec data home.
|
|
5522
|
+
* The inputs are hashed so the returned URI never exposes either component; only pair equality matters.
|
|
5523
|
+
*/
|
|
5524
|
+
function computeEnvUri(options) {
|
|
5525
|
+
const dataHome = options.dataHome ?? "";
|
|
5526
|
+
return asEnvUri(`openspecui-env://1/${createHash("sha256").update(`${options.hostIdentity}\u0000${dataHome}`).digest("hex")}`);
|
|
5527
|
+
}
|
|
5528
|
+
/** Required byte length of a generated `--auth` credential (256 bits of entropy). */
|
|
5529
|
+
const ACCESS_GATE_CREDENTIAL_BYTES = 32;
|
|
5530
|
+
/** Base64url-encode without padding. */
|
|
5531
|
+
function base64url(bytes) {
|
|
5532
|
+
return Buffer.from(bytes).toString("base64url");
|
|
5533
|
+
}
|
|
5534
|
+
/** Compute a short SHA-256 fingerprint suitable for safe display. */
|
|
5535
|
+
function accessGateFingerprint(credential) {
|
|
5536
|
+
return createHash("sha256").update(credential).digest("hex").slice(0, 16);
|
|
5537
|
+
}
|
|
5538
|
+
/** Generate a high-entropy Bearer credential for `--auth` and return the complete gate record. */
|
|
5539
|
+
function generateAccessGateCredential() {
|
|
5540
|
+
const credential = base64url(randomBytes(ACCESS_GATE_CREDENTIAL_BYTES));
|
|
5541
|
+
return {
|
|
5542
|
+
credential,
|
|
5543
|
+
fingerprint: accessGateFingerprint(credential),
|
|
5544
|
+
authorizationHeader: `Bearer ${credential}`
|
|
5545
|
+
};
|
|
5546
|
+
}
|
|
5547
|
+
/**
|
|
5548
|
+
* Normalize an operator-supplied `--password` secret into the same Bearer gate record. The secret is
|
|
5549
|
+
* used verbatim as the credential (no hashing) so the gate is a single shared secret comparison.
|
|
5550
|
+
*/
|
|
5551
|
+
function normalizeAccessGatePassword(password) {
|
|
5552
|
+
const credential = password;
|
|
5553
|
+
return {
|
|
5554
|
+
credential,
|
|
5555
|
+
fingerprint: accessGateFingerprint(credential),
|
|
5556
|
+
authorizationHeader: `Bearer ${credential}`
|
|
5557
|
+
};
|
|
5558
|
+
}
|
|
5559
|
+
/** Constant-time comparison of two equal-length secrets to mitigate timing side channels. */
|
|
5560
|
+
function constantTimeEqual(a, b) {
|
|
5561
|
+
const left = Buffer.from(a);
|
|
5562
|
+
const right = Buffer.from(b);
|
|
5563
|
+
if (left.length !== right.length) {
|
|
5564
|
+
left.compare(left);
|
|
5565
|
+
return false;
|
|
5566
|
+
}
|
|
5567
|
+
return left.compare(right) === 0;
|
|
5568
|
+
}
|
|
4791
5569
|
|
|
4792
5570
|
//#endregion
|
|
4793
|
-
//#region src/
|
|
4794
|
-
function
|
|
4795
|
-
const
|
|
4796
|
-
|
|
4797
|
-
|
|
4798
|
-
|
|
4799
|
-
|
|
4800
|
-
|
|
4801
|
-
|
|
4802
|
-
|
|
5571
|
+
//#region src/snapshot-redaction.ts
|
|
5572
|
+
function isAbsoluteFsPath$1(path) {
|
|
5573
|
+
const normalized = path.replace(/\\/g, "/");
|
|
5574
|
+
return normalized.startsWith("/") || /^[A-Za-z]:\//.test(normalized);
|
|
5575
|
+
}
|
|
5576
|
+
/**
|
|
5577
|
+
* Redact an absolute path to a display-safe relative fragment. Absolute filesystem locations must
|
|
5578
|
+
* never survive into a published snapshot; only the trailing relative segment is retained so a reader
|
|
5579
|
+
* still understands the artifact's repository-relative location.
|
|
5580
|
+
*/
|
|
5581
|
+
function redactPath(raw) {
|
|
5582
|
+
if (!raw) return raw;
|
|
5583
|
+
const normalized = raw.replace(/\\/g, "/");
|
|
5584
|
+
if (!isAbsoluteFsPath$1(normalized)) return normalized;
|
|
5585
|
+
return normalized.split("/").filter(Boolean).slice(-2).join("/");
|
|
5586
|
+
}
|
|
5587
|
+
/** Deep-clone helper that preserves JSON-serializable snapshot shape without mutating input. */
|
|
5588
|
+
function cloneSnapshot(snapshot) {
|
|
5589
|
+
return JSON.parse(JSON.stringify(snapshot));
|
|
5590
|
+
}
|
|
5591
|
+
/**
|
|
5592
|
+
* Produce a publication-safe copy of `snapshot`. Removes absolute project/Store/data-home/registry
|
|
5593
|
+
* paths, host identity, and raw path-bearing diagnostics, and clears the Git remote URL unless the
|
|
5594
|
+
* Reference export policy is explicitly `include`. Display-safe relative paths (`displayPath`,
|
|
5595
|
+
* repository-relative fragments) and human-readable labels are preserved.
|
|
5596
|
+
*
|
|
5597
|
+
* This is the single redaction boundary for static export privacy (Section 7.8). Callers must run it
|
|
5598
|
+
* immediately before serializing the published `data.json` / SSG site.
|
|
5599
|
+
*/
|
|
5600
|
+
function redactSnapshotForPublication(snapshot) {
|
|
5601
|
+
const redacted = cloneSnapshot(snapshot);
|
|
5602
|
+
const root = redacted.meta.root;
|
|
5603
|
+
if (root && root.planningRootPath) root.planningRootPath = redactPath(root.planningRootPath);
|
|
5604
|
+
if (redacted.git && redacted.meta.referencePolicy?.kind !== "include") redacted.git.repositoryUrl = null;
|
|
5605
|
+
const opsx = redacted.opsx;
|
|
5606
|
+
if (opsx) {
|
|
5607
|
+
for (const resolution of Object.values(opsx.schemaResolutions)) {
|
|
5608
|
+
if (typeof resolution.path === "string") resolution.path = redactPath(resolution.path);
|
|
5609
|
+
for (const shadow of resolution.shadows ?? []) if (typeof shadow.path === "string") shadow.path = redactPath(shadow.path);
|
|
5610
|
+
}
|
|
5611
|
+
for (const artifactMap of Object.values(opsx.templates)) for (const info of Object.values(artifactMap)) if (typeof info.path === "string") info.path = redactPath(info.path);
|
|
5612
|
+
if (opsx.templateContents) {
|
|
5613
|
+
for (const artifactMap of Object.values(opsx.templateContents)) for (const entry of Object.values(artifactMap)) if (typeof entry.path === "string") entry.path = redactPath(entry.path);
|
|
5614
|
+
}
|
|
4803
5615
|
}
|
|
4804
|
-
|
|
4805
|
-
|
|
4806
|
-
|
|
5616
|
+
if (opsx?.changeMetadata) {
|
|
5617
|
+
for (const [id, raw] of Object.entries(opsx.changeMetadata)) if (typeof raw === "string") opsx.changeMetadata[id] = raw.replace(/(?:\/[\w.\-@]+){2,}(?=\s|"|$)/g, (match) => redactPath(match));
|
|
5618
|
+
}
|
|
5619
|
+
return redacted;
|
|
5620
|
+
}
|
|
5621
|
+
/** True when any string field in the snapshot still carries an absolute filesystem path. */
|
|
5622
|
+
function snapshotHasAbsolutePath(snapshot) {
|
|
5623
|
+
const text = JSON.stringify(snapshot);
|
|
5624
|
+
return /\b(?:\/[\w.\-@]+){2,}/.test(text) || /[A-Za-z]:\//.test(text);
|
|
4807
5625
|
}
|
|
5626
|
+
|
|
5627
|
+
//#endregion
|
|
5628
|
+
//#region src/opsx-kernel.ts
|
|
5629
|
+
/**
|
|
5630
|
+
* Orthogonal intents (updated 2026-07-27 Asia/Shanghai):
|
|
5631
|
+
* 1. Maintain reactive CLI-backed projections and their direct typed Projection Work readers.
|
|
5632
|
+
* 2. Share and release per-entity streams through one planning-root kernel lifecycle.
|
|
5633
|
+
* 3. Keep OpenSpec configuration ownership outside the workflow projection cache.
|
|
5634
|
+
* 4. Preserve typed Status/Instructions provenance with the resolved root selector.
|
|
5635
|
+
* 5. Keep Change enumeration business truth in the typed CLI result while files only invalidate it.
|
|
5636
|
+
*
|
|
5637
|
+
* Original request (2026-07-15): "Planning-root adapters and services consume the CLI-resolved root."
|
|
5638
|
+
* Original request (2026-07-23): "OPSX Status 不应等待完整 Kernel warmup,且必须保留 CLI evidence。"
|
|
5639
|
+
* Original request (2026-07-26): "展开全面的接口升级和内核升级和测试升级。"
|
|
5640
|
+
*/
|
|
4808
5641
|
function toRelativePath(root, absolutePath) {
|
|
4809
5642
|
return relative(root, absolutePath).split(sep).join("/");
|
|
4810
5643
|
}
|
|
@@ -4849,6 +5682,12 @@ async function readEntriesUnderRoot(root) {
|
|
|
4849
5682
|
};
|
|
4850
5683
|
return collectEntries(root);
|
|
4851
5684
|
}
|
|
5685
|
+
function requireCanonicalArtifactLocation(changeId, outputPath) {
|
|
5686
|
+
return {
|
|
5687
|
+
changeId: requireCanonicalOpenSpecEntityId(changeId, "changeId"),
|
|
5688
|
+
outputPath: requireOpenSpecEntityRelativePath(outputPath, "outputPath")
|
|
5689
|
+
};
|
|
5690
|
+
}
|
|
4852
5691
|
function splitRelativePathSegments(path) {
|
|
4853
5692
|
return path.replace(/\\/g, "/").split("/").filter(Boolean);
|
|
4854
5693
|
}
|
|
@@ -4861,7 +5700,8 @@ function getGlobStaticPrefix(outputPath) {
|
|
|
4861
5700
|
return lastSlashIndex === -1 ? "" : staticPrefix.slice(0, lastSlashIndex);
|
|
4862
5701
|
}
|
|
4863
5702
|
async function readGlobArtifactFiles(projectDir, changeId, outputPath) {
|
|
4864
|
-
|
|
5703
|
+
const location = requireCanonicalArtifactLocation(changeId, outputPath);
|
|
5704
|
+
return (await readEntriesUnderRoot(join$1(projectDir, "openspec", "changes", location.changeId))).filter((entry) => entry.type === "file" && matchesGlob(entry.path, location.outputPath)).map((entry) => ({
|
|
4865
5705
|
path: entry.path,
|
|
4866
5706
|
type: "file",
|
|
4867
5707
|
content: entry.content ?? ""
|
|
@@ -4904,8 +5744,9 @@ async function touchDirectoryTree(rootDir) {
|
|
|
4904
5744
|
}));
|
|
4905
5745
|
}
|
|
4906
5746
|
async function touchArtifactOutputDeps(projectDir, changeId, outputPath) {
|
|
4907
|
-
const
|
|
4908
|
-
const
|
|
5747
|
+
const location = requireCanonicalArtifactLocation(changeId, outputPath);
|
|
5748
|
+
const changeDir = join$1(projectDir, "openspec", "changes", location.changeId);
|
|
5749
|
+
const normalizedOutputPath = location.outputPath;
|
|
4909
5750
|
if (isGlobPattern(normalizedOutputPath)) {
|
|
4910
5751
|
const staticPrefix = getGlobStaticPrefix(normalizedOutputPath);
|
|
4911
5752
|
if (staticPrefix) await touchDirectoryPathDeps(changeDir, staticPrefix);
|
|
@@ -4916,16 +5757,44 @@ async function touchArtifactOutputDeps(projectDir, changeId, outputPath) {
|
|
|
4916
5757
|
if (parentPath) await touchDirectoryPathDeps(changeDir, parentPath);
|
|
4917
5758
|
await reactiveExists(join$1(changeDir, normalizedOutputPath));
|
|
4918
5759
|
}
|
|
5760
|
+
function requireCommandData(label, result, schema) {
|
|
5761
|
+
const parsed = schema.safeParse(result.data);
|
|
5762
|
+
if (result.success && parsed.success) return parsed.data;
|
|
5763
|
+
throw new CliProjectionCommandError(result.contractError || result.stderr.trim() || result.diagnostics.map((diagnostic) => diagnostic.message).join("\n") || `${label} failed (exit ${result.exitCode ?? "null"})`, result);
|
|
5764
|
+
}
|
|
5765
|
+
function readOpsxRoot(data) {
|
|
5766
|
+
const parsed = z.object({ root: CliRootSchema }).passthrough().safeParse(data);
|
|
5767
|
+
return parsed.success ? parsed.data.root : void 0;
|
|
5768
|
+
}
|
|
5769
|
+
function createOpsxCliEvidence(result, command, rootSelector) {
|
|
5770
|
+
const root = readOpsxRoot(result.data);
|
|
5771
|
+
return OpsxCliEvidenceSchema.parse({
|
|
5772
|
+
command,
|
|
5773
|
+
success: result.success,
|
|
5774
|
+
stdout: result.stdout,
|
|
5775
|
+
stderr: result.stderr,
|
|
5776
|
+
exitCode: result.exitCode,
|
|
5777
|
+
payload: result.payload,
|
|
5778
|
+
diagnostics: result.diagnostics,
|
|
5779
|
+
...result.contractError ? { contractError: result.contractError } : {},
|
|
5780
|
+
selector: rootSelector.store === void 0 ? {} : { store: rootSelector.store },
|
|
5781
|
+
...root ? { root } : {}
|
|
5782
|
+
});
|
|
5783
|
+
}
|
|
5784
|
+
function createOpsxStatusEvidence(result, rootSelector) {
|
|
5785
|
+
return OpsxStatusEvidenceSchema.parse(createOpsxCliEvidence(result, "status", rootSelector));
|
|
5786
|
+
}
|
|
4919
5787
|
var OpsxKernel = class {
|
|
4920
5788
|
projectDir;
|
|
4921
5789
|
cliExecutor;
|
|
5790
|
+
runtimeInvalidation;
|
|
5791
|
+
rootSelector;
|
|
4922
5792
|
controller = new AbortController();
|
|
4923
5793
|
warmupPromise = null;
|
|
4924
5794
|
_streamReady = /* @__PURE__ */ new Map();
|
|
4925
5795
|
_statusList = new ReactiveState([]);
|
|
4926
5796
|
_schemas = new ReactiveState([]);
|
|
4927
5797
|
_changeIds = new ReactiveState([]);
|
|
4928
|
-
_projectConfig = new ReactiveState(null);
|
|
4929
5798
|
_schemaResolutions = /* @__PURE__ */ new Map();
|
|
4930
5799
|
_schemaDetails = /* @__PURE__ */ new Map();
|
|
4931
5800
|
_schemaFiles = /* @__PURE__ */ new Map();
|
|
@@ -4939,9 +5808,11 @@ var OpsxKernel = class {
|
|
|
4939
5808
|
_artifactOutputs = /* @__PURE__ */ new Map();
|
|
4940
5809
|
_globArtifactFiles = /* @__PURE__ */ new Map();
|
|
4941
5810
|
_entityControllers = /* @__PURE__ */ new Map();
|
|
4942
|
-
constructor(projectDir, cliExecutor) {
|
|
5811
|
+
constructor(projectDir, cliExecutor, runtimeInvalidation, rootSelector) {
|
|
4943
5812
|
this.projectDir = projectDir;
|
|
4944
5813
|
this.cliExecutor = cliExecutor;
|
|
5814
|
+
this.runtimeInvalidation = runtimeInvalidation;
|
|
5815
|
+
this.rootSelector = rootSelector;
|
|
4945
5816
|
}
|
|
4946
5817
|
async warmup() {
|
|
4947
5818
|
if (this.warmupPromise) return this.warmupPromise;
|
|
@@ -4956,11 +5827,7 @@ var OpsxKernel = class {
|
|
|
4956
5827
|
}
|
|
4957
5828
|
async runWarmup() {
|
|
4958
5829
|
const signal = this.controller.signal;
|
|
4959
|
-
await Promise.all([
|
|
4960
|
-
this.startStreamOnce("global:schemas", this._schemas, () => this.fetchSchemas(), signal),
|
|
4961
|
-
this.startStreamOnce("global:change-ids", this._changeIds, () => this.fetchChangeIds(), signal),
|
|
4962
|
-
this.startStreamOnce("global:project-config", this._projectConfig, () => this.fetchProjectConfig(), signal)
|
|
4963
|
-
]);
|
|
5830
|
+
await Promise.all([this.startStreamOnce("global:schemas", this._schemas, () => this.fetchSchemas(), signal), this.startStreamOnce("global:change-ids", this._changeIds, () => this.fetchChangeIds(), signal)]);
|
|
4964
5831
|
const schemas = this._schemas.get();
|
|
4965
5832
|
await Promise.all(schemas.map((s) => this.warmupSchema(s.name, signal)));
|
|
4966
5833
|
const changeIds = this._changeIds.get();
|
|
@@ -4985,9 +5852,6 @@ var OpsxKernel = class {
|
|
|
4985
5852
|
getChangeIds() {
|
|
4986
5853
|
return this._changeIds.get();
|
|
4987
5854
|
}
|
|
4988
|
-
getProjectConfig() {
|
|
4989
|
-
return this._projectConfig.get();
|
|
4990
|
-
}
|
|
4991
5855
|
getTemplates(schema) {
|
|
4992
5856
|
const key = schema ?? "";
|
|
4993
5857
|
const state = this._templates.get(key);
|
|
@@ -5224,10 +6088,16 @@ var OpsxKernel = class {
|
|
|
5224
6088
|
for (const key of this._streamReady.keys()) if (key.startsWith(prefix)) this._streamReady.delete(key);
|
|
5225
6089
|
}
|
|
5226
6090
|
async fetchSchemas() {
|
|
6091
|
+
return (await this.fetchSchemasProjection()).value;
|
|
6092
|
+
}
|
|
6093
|
+
async fetchSchemasProjection() {
|
|
6094
|
+
this.runtimeInvalidation.track("schemas");
|
|
5227
6095
|
await touchOpsxProjectDeps(this.projectDir);
|
|
5228
|
-
const result = await this.cliExecutor.schemas();
|
|
5229
|
-
|
|
5230
|
-
|
|
6096
|
+
const result = await this.cliExecutor.contracts.schemas();
|
|
6097
|
+
return {
|
|
6098
|
+
value: requireCommandData("openspec schemas", result, CliSchemasSchema),
|
|
6099
|
+
evidence: toCliProjectionCommandEvidence(result)
|
|
6100
|
+
};
|
|
5231
6101
|
}
|
|
5232
6102
|
async fetchChangeIds() {
|
|
5233
6103
|
return reactiveReadDir(join$1(this.projectDir, "openspec", "changes"), {
|
|
@@ -5236,22 +6106,40 @@ var OpsxKernel = class {
|
|
|
5236
6106
|
exclude: ["archive"]
|
|
5237
6107
|
});
|
|
5238
6108
|
}
|
|
5239
|
-
async
|
|
5240
|
-
|
|
6109
|
+
async fetchChangeListProjection() {
|
|
6110
|
+
await this.fetchChangeIds();
|
|
6111
|
+
const result = await this.cliExecutor.contracts.listChanges(this.rootSelector);
|
|
6112
|
+
return {
|
|
6113
|
+
value: requireCommandData("openspec list", result, CliChangeListSchema).changes.map(({ name }) => name),
|
|
6114
|
+
evidence: toCliProjectionCommandEvidence(result)
|
|
6115
|
+
};
|
|
5241
6116
|
}
|
|
5242
6117
|
async fetchStatus(changeId, schema) {
|
|
5243
6118
|
await touchOpsxProjectDeps(this.projectDir);
|
|
5244
6119
|
await touchOpsxChangeDeps(this.projectDir, changeId);
|
|
5245
|
-
const
|
|
5246
|
-
|
|
5247
|
-
|
|
5248
|
-
|
|
5249
|
-
|
|
5250
|
-
|
|
5251
|
-
|
|
5252
|
-
|
|
5253
|
-
|
|
5254
|
-
|
|
6120
|
+
const result = await this.cliExecutor.contracts.workflowStatus(changeId, {
|
|
6121
|
+
...this.rootSelector,
|
|
6122
|
+
...schema ? { schema } : {}
|
|
6123
|
+
});
|
|
6124
|
+
const evidence = createOpsxStatusEvidence(result, this.rootSelector);
|
|
6125
|
+
const data = requireCommandData("openspec status", result, CliWorkflowStatusSuccessSchema);
|
|
6126
|
+
const status = ChangeStatusSchema.parse({
|
|
6127
|
+
changeName: data.changeName,
|
|
6128
|
+
schemaName: data.schemaName,
|
|
6129
|
+
isComplete: data.isComplete,
|
|
6130
|
+
applyRequires: data.applyRequires,
|
|
6131
|
+
artifacts: data.artifacts,
|
|
6132
|
+
provenance: {
|
|
6133
|
+
kind: "cli",
|
|
6134
|
+
planningHome: data.planningHome,
|
|
6135
|
+
changeRoot: data.changeRoot,
|
|
6136
|
+
artifactPaths: data.artifactPaths,
|
|
6137
|
+
nextSteps: data.nextSteps,
|
|
6138
|
+
actionContext: data.actionContext,
|
|
6139
|
+
root: data.root,
|
|
6140
|
+
evidence
|
|
6141
|
+
}
|
|
6142
|
+
});
|
|
5255
6143
|
const changeRelDir = `openspec/changes/${changeId}`;
|
|
5256
6144
|
for (const artifact of status.artifacts) {
|
|
5257
6145
|
artifact.relativePath = `${changeRelDir}/${artifact.outputPath}`;
|
|
@@ -5268,57 +6156,71 @@ var OpsxKernel = class {
|
|
|
5268
6156
|
async fetchInstructions(changeId, artifact, schema) {
|
|
5269
6157
|
await touchOpsxProjectDeps(this.projectDir);
|
|
5270
6158
|
await touchOpsxChangeDeps(this.projectDir, changeId);
|
|
5271
|
-
const
|
|
5272
|
-
|
|
5273
|
-
|
|
5274
|
-
|
|
5275
|
-
|
|
5276
|
-
|
|
5277
|
-
|
|
5278
|
-
|
|
5279
|
-
|
|
5280
|
-
if (!result.success) throw new Error(result.stderr || `openspec instructions failed (exit ${result.exitCode ?? "null"})`);
|
|
5281
|
-
return parseCliJson(result.stdout, ArtifactInstructionsSchema, "openspec instructions");
|
|
6159
|
+
const result = await this.cliExecutor.contracts.artifactInstructions(changeId, artifact, {
|
|
6160
|
+
...this.rootSelector,
|
|
6161
|
+
...schema ? { schema } : {}
|
|
6162
|
+
});
|
|
6163
|
+
const data = requireCommandData("openspec instructions", result, CliArtifactInstructionsSuccessSchema);
|
|
6164
|
+
return ArtifactInstructionsSchema.parse({
|
|
6165
|
+
...data,
|
|
6166
|
+
evidence: createOpsxCliEvidence(result, "instructions", this.rootSelector)
|
|
6167
|
+
});
|
|
5282
6168
|
}
|
|
5283
6169
|
async fetchApplyInstructions(changeId, schema) {
|
|
5284
6170
|
await touchOpsxProjectDeps(this.projectDir);
|
|
5285
6171
|
await touchOpsxChangeDeps(this.projectDir, changeId);
|
|
5286
|
-
const
|
|
5287
|
-
|
|
5288
|
-
|
|
5289
|
-
|
|
5290
|
-
|
|
5291
|
-
|
|
5292
|
-
|
|
5293
|
-
|
|
5294
|
-
|
|
5295
|
-
|
|
5296
|
-
|
|
6172
|
+
const result = await this.cliExecutor.contracts.applyInstructions(changeId, {
|
|
6173
|
+
...this.rootSelector,
|
|
6174
|
+
...schema ? { schema } : {}
|
|
6175
|
+
});
|
|
6176
|
+
const data = requireCommandData("openspec instructions apply", result, CliApplyInstructionsSuccessSchema);
|
|
6177
|
+
const instructions = ApplyInstructionsSchema.parse({
|
|
6178
|
+
...data,
|
|
6179
|
+
evidence: createOpsxCliEvidence(result, "instructions apply", this.rootSelector)
|
|
6180
|
+
});
|
|
6181
|
+
const changeDir = join$1(this.projectDir, "openspec", "changes", changeId);
|
|
6182
|
+
const [files, schemaDetail] = await Promise.all([readEntriesUnderRoot(changeDir), this.fetchSchemaResolution(instructions.schemaName).then((resolution) => this.fetchSchemaDetailAtResolution(instructions.schemaName, resolution)).catch(() => null)]);
|
|
6183
|
+
const { trackedTaskProgress } = projectTaskProjectionsFromMarkdownFiles(files, {
|
|
6184
|
+
schemaDetail,
|
|
6185
|
+
hasSchemaMetadata: true
|
|
6186
|
+
});
|
|
6187
|
+
return {
|
|
6188
|
+
...instructions,
|
|
6189
|
+
applyInstructionProgress: createApplyInstructionProgress(instructions.applyInstructionProgress, trackedTaskProgress)
|
|
6190
|
+
};
|
|
5297
6191
|
}
|
|
5298
6192
|
async fetchSchemaResolution(name) {
|
|
6193
|
+
return (await this.fetchSchemaResolutionProjection(name)).value;
|
|
6194
|
+
}
|
|
6195
|
+
async fetchSchemaResolutionProjection(name) {
|
|
5299
6196
|
await touchOpsxProjectDeps(this.projectDir);
|
|
5300
|
-
const result = await this.cliExecutor.schemaWhich(name);
|
|
5301
|
-
|
|
5302
|
-
const parsed = parseCliJson(result.stdout, SchemaResolutionSchema, "openspec schema which");
|
|
6197
|
+
const result = await this.cliExecutor.contracts.schemaWhich(name);
|
|
6198
|
+
const value = requireCommandData("openspec schema which", result, SchemaResolutionSchema);
|
|
5303
6199
|
return {
|
|
5304
|
-
|
|
5305
|
-
|
|
5306
|
-
|
|
5307
|
-
|
|
5308
|
-
|
|
5309
|
-
shadows: parsed.shadows.map((shadow) => ({
|
|
5310
|
-
...shadow,
|
|
5311
|
-
displayPath: toOpsxDisplayPath(shadow.path, {
|
|
5312
|
-
source: shadow.source,
|
|
6200
|
+
evidence: toCliProjectionCommandEvidence(result),
|
|
6201
|
+
value: {
|
|
6202
|
+
...value,
|
|
6203
|
+
displayPath: toOpsxDisplayPath(value.path, {
|
|
6204
|
+
source: value.source,
|
|
5313
6205
|
projectDir: this.projectDir
|
|
5314
|
-
})
|
|
5315
|
-
|
|
6206
|
+
}),
|
|
6207
|
+
shadows: value.shadows.map((shadow) => ({
|
|
6208
|
+
...shadow,
|
|
6209
|
+
displayPath: toOpsxDisplayPath(shadow.path, {
|
|
6210
|
+
source: shadow.source,
|
|
6211
|
+
projectDir: this.projectDir
|
|
6212
|
+
})
|
|
6213
|
+
}))
|
|
6214
|
+
}
|
|
5316
6215
|
};
|
|
5317
6216
|
}
|
|
5318
6217
|
async fetchSchemaDetail(name) {
|
|
5319
6218
|
await touchOpsxProjectDeps(this.projectDir);
|
|
5320
6219
|
await this.ensureSchemaResolution(name);
|
|
5321
|
-
|
|
6220
|
+
return this.fetchSchemaDetailAtResolution(name, this.getSchemaResolution(name));
|
|
6221
|
+
}
|
|
6222
|
+
async fetchSchemaDetailAtResolution(name, resolution) {
|
|
6223
|
+
const schemaPath = join$1(resolution.path, "schema.yaml");
|
|
5322
6224
|
const content = await reactiveReadFile(schemaPath);
|
|
5323
6225
|
if (!content) throw new Error(`schema.yaml not found at ${schemaPath}`);
|
|
5324
6226
|
return parseSchemaYamlInline(content, name, schemaPath);
|
|
@@ -5334,22 +6236,29 @@ var OpsxKernel = class {
|
|
|
5334
6236
|
return reactiveReadFile(join$1(this.getSchemaResolution(name).path, "schema.yaml"));
|
|
5335
6237
|
}
|
|
5336
6238
|
async fetchTemplates(schema) {
|
|
6239
|
+
return (await this.fetchTemplatesProjection(schema)).value;
|
|
6240
|
+
}
|
|
6241
|
+
async fetchTemplatesProjection(schema) {
|
|
5337
6242
|
await touchOpsxProjectDeps(this.projectDir);
|
|
5338
|
-
const result = await this.cliExecutor.templates(schema);
|
|
5339
|
-
|
|
5340
|
-
|
|
5341
|
-
|
|
5342
|
-
|
|
5343
|
-
|
|
5344
|
-
|
|
5345
|
-
|
|
5346
|
-
|
|
5347
|
-
|
|
5348
|
-
|
|
6243
|
+
const result = await this.cliExecutor.contracts.templates(schema);
|
|
6244
|
+
const templates = requireCommandData("openspec templates", result, TemplatesSchema);
|
|
6245
|
+
return {
|
|
6246
|
+
value: Object.fromEntries(Object.entries(templates).map(([artifactId, info]) => [artifactId, {
|
|
6247
|
+
...info,
|
|
6248
|
+
path: toAbsoluteProjectPath(this.projectDir, info.path),
|
|
6249
|
+
displayPath: toOpsxDisplayPath(info.path, {
|
|
6250
|
+
source: info.source,
|
|
6251
|
+
projectDir: this.projectDir
|
|
6252
|
+
})
|
|
6253
|
+
}])),
|
|
6254
|
+
evidence: toCliProjectionCommandEvidence(result)
|
|
6255
|
+
};
|
|
5349
6256
|
}
|
|
5350
6257
|
async fetchTemplateContents(schema) {
|
|
5351
6258
|
await this.ensureTemplates(schema);
|
|
5352
|
-
|
|
6259
|
+
return this.readTemplateContents(this.getTemplates(schema));
|
|
6260
|
+
}
|
|
6261
|
+
async readTemplateContents(templates) {
|
|
5353
6262
|
const entries = await Promise.all(Object.entries(templates).map(async ([artifactId, info]) => {
|
|
5354
6263
|
return [artifactId, {
|
|
5355
6264
|
content: await reactiveReadFile(info.path),
|
|
@@ -5367,7 +6276,67 @@ var OpsxKernel = class {
|
|
|
5367
6276
|
return reactiveReadFile(join$1(this.projectDir, "openspec", "changes", changeId, ".openspec.yaml"));
|
|
5368
6277
|
}
|
|
5369
6278
|
async fetchArtifactOutput(changeId, outputPath) {
|
|
5370
|
-
|
|
6279
|
+
const location = requireCanonicalArtifactLocation(changeId, outputPath);
|
|
6280
|
+
return reactiveReadFile(join$1(this.projectDir, "openspec", "changes", location.changeId, location.outputPath));
|
|
6281
|
+
}
|
|
6282
|
+
/** Execute one Status projection with dependencies owned by the caller's reactive generation. */
|
|
6283
|
+
readStatusProjection(changeId, schema) {
|
|
6284
|
+
return this.fetchStatus(changeId, schema);
|
|
6285
|
+
}
|
|
6286
|
+
/** Execute CLI-owned Change enumeration with file dependencies owned by the caller's Work generation. */
|
|
6287
|
+
readChangeListProjection() {
|
|
6288
|
+
return this.fetchChangeListProjection();
|
|
6289
|
+
}
|
|
6290
|
+
/** Execute the current Change Status list without joining the Kernel's retained entity streams. */
|
|
6291
|
+
async readStatusListProjection() {
|
|
6292
|
+
const changeList = await this.fetchChangeListProjection();
|
|
6293
|
+
return {
|
|
6294
|
+
value: await Promise.all(changeList.value.map((changeId) => this.fetchStatus(changeId))),
|
|
6295
|
+
evidence: changeList.evidence
|
|
6296
|
+
};
|
|
6297
|
+
}
|
|
6298
|
+
/** Execute one artifact-instructions projection in the caller-owned Work generation. */
|
|
6299
|
+
readInstructionsProjection(changeId, artifact, schema) {
|
|
6300
|
+
return this.fetchInstructions(changeId, artifact, schema);
|
|
6301
|
+
}
|
|
6302
|
+
/** Execute one Apply-instructions projection in the caller-owned Work generation. */
|
|
6303
|
+
readApplyInstructionsProjection(changeId, schema) {
|
|
6304
|
+
return this.fetchApplyInstructions(changeId, schema);
|
|
6305
|
+
}
|
|
6306
|
+
/** Execute the aggregate Schema bundle while retaining exact resolved-file dependencies. */
|
|
6307
|
+
async readConfigBundleProjection() {
|
|
6308
|
+
const schemasProjection = await this.fetchSchemasProjection();
|
|
6309
|
+
const details = await Promise.all(schemasProjection.value.map(async ({ name }) => {
|
|
6310
|
+
const resolution = await this.fetchSchemaResolutionProjection(name);
|
|
6311
|
+
return {
|
|
6312
|
+
name,
|
|
6313
|
+
detail: await this.fetchSchemaDetailAtResolution(name, resolution.value).catch(() => null),
|
|
6314
|
+
resolution
|
|
6315
|
+
};
|
|
6316
|
+
}));
|
|
6317
|
+
return {
|
|
6318
|
+
value: {
|
|
6319
|
+
schemas: schemasProjection.value,
|
|
6320
|
+
schemaDetails: Object.fromEntries(details.map(({ name, detail }) => [name, detail])),
|
|
6321
|
+
schemaResolutions: Object.fromEntries(details.map(({ name, resolution }) => [name, resolution.value]))
|
|
6322
|
+
},
|
|
6323
|
+
evidence: {
|
|
6324
|
+
schemas: schemasProjection.evidence,
|
|
6325
|
+
schemaResolutions: Object.fromEntries(details.map(({ name, resolution }) => [name, resolution.evidence]))
|
|
6326
|
+
}
|
|
6327
|
+
};
|
|
6328
|
+
}
|
|
6329
|
+
/** Execute one CLI template-index projection. */
|
|
6330
|
+
readTemplatesProjection(schema) {
|
|
6331
|
+
return this.fetchTemplatesProjection(schema);
|
|
6332
|
+
}
|
|
6333
|
+
/** Execute one template-content projection from the CLI-selected template index. */
|
|
6334
|
+
async readTemplateContentsProjection(schema) {
|
|
6335
|
+
const templates = await this.fetchTemplatesProjection(schema);
|
|
6336
|
+
return {
|
|
6337
|
+
value: await this.readTemplateContents(templates.value),
|
|
6338
|
+
evidence: templates.evidence
|
|
6339
|
+
};
|
|
5371
6340
|
}
|
|
5372
6341
|
async ensureSchemas() {
|
|
5373
6342
|
await this.startStreamOnce("global:schemas", this._schemas, () => this.fetchSchemas(), this.controller.signal);
|
|
@@ -5375,36 +6344,38 @@ var OpsxKernel = class {
|
|
|
5375
6344
|
async ensureChangeIds() {
|
|
5376
6345
|
await this.startStreamOnce("global:change-ids", this._changeIds, () => this.fetchChangeIds(), this.controller.signal);
|
|
5377
6346
|
}
|
|
5378
|
-
async ensureProjectConfig() {
|
|
5379
|
-
await this.startStreamOnce("global:project-config", this._projectConfig, () => this.fetchProjectConfig(), this.controller.signal);
|
|
5380
|
-
}
|
|
5381
6347
|
async ensureStatusList() {
|
|
5382
6348
|
await this.startStreamOnce("global:status-list", this._statusList, () => this.fetchStatusList(), this.controller.signal);
|
|
5383
6349
|
}
|
|
5384
6350
|
async ensureStatus(changeId, schema) {
|
|
5385
|
-
const
|
|
6351
|
+
const canonicalChangeId = requireCanonicalOpenSpecEntityId(changeId, "changeId");
|
|
6352
|
+
const key = `${canonicalChangeId}:${schema ?? ""}`;
|
|
5386
6353
|
if (!this._statuses.has(key)) this._statuses.set(key, new ReactiveState(null));
|
|
5387
|
-
await this.startStreamOnce(`change:${
|
|
6354
|
+
await this.startStreamOnce(`change:${canonicalChangeId}:status:${schema ?? ""}`, this._statuses.get(key), () => this.fetchStatus(canonicalChangeId, schema), this.controller.signal);
|
|
5388
6355
|
}
|
|
5389
6356
|
async ensureInstructions(changeId, artifact, schema) {
|
|
5390
|
-
const
|
|
6357
|
+
const canonicalChangeId = requireCanonicalOpenSpecEntityId(changeId, "changeId");
|
|
6358
|
+
const key = `${canonicalChangeId}:${artifact}:${schema ?? ""}`;
|
|
5391
6359
|
if (!this._instructions.has(key)) this._instructions.set(key, new ReactiveState(null));
|
|
5392
|
-
await this.startStreamOnce(`change:${
|
|
6360
|
+
await this.startStreamOnce(`change:${canonicalChangeId}:instructions:${artifact}:${schema ?? ""}`, this._instructions.get(key), () => this.fetchInstructions(canonicalChangeId, artifact, schema), this.controller.signal);
|
|
5393
6361
|
}
|
|
5394
6362
|
async ensureApplyInstructions(changeId, schema) {
|
|
5395
|
-
const
|
|
6363
|
+
const canonicalChangeId = requireCanonicalOpenSpecEntityId(changeId, "changeId");
|
|
6364
|
+
const key = `${canonicalChangeId}:${schema ?? ""}`;
|
|
5396
6365
|
if (!this._applyInstructions.has(key)) this._applyInstructions.set(key, new ReactiveState(null));
|
|
5397
|
-
await this.startStreamOnce(`change:${
|
|
6366
|
+
await this.startStreamOnce(`change:${canonicalChangeId}:apply:${schema ?? ""}`, this._applyInstructions.get(key), () => this.fetchApplyInstructions(canonicalChangeId, schema), this.controller.signal);
|
|
5398
6367
|
}
|
|
5399
6368
|
async ensureArtifactOutput(changeId, outputPath) {
|
|
5400
|
-
const
|
|
6369
|
+
const location = requireCanonicalArtifactLocation(changeId, outputPath);
|
|
6370
|
+
const key = `${location.changeId}:${location.outputPath}`;
|
|
5401
6371
|
if (!this._artifactOutputs.has(key)) this._artifactOutputs.set(key, new ReactiveState(null));
|
|
5402
|
-
await this.startStreamOnce(`change:${changeId}:output:${outputPath}`, this._artifactOutputs.get(key), () => this.fetchArtifactOutput(changeId, outputPath), this.controller.signal);
|
|
6372
|
+
await this.startStreamOnce(`change:${location.changeId}:output:${location.outputPath}`, this._artifactOutputs.get(key), () => this.fetchArtifactOutput(location.changeId, location.outputPath), this.controller.signal);
|
|
5403
6373
|
}
|
|
5404
6374
|
async ensureGlobArtifactFiles(changeId, outputPath) {
|
|
5405
|
-
const
|
|
6375
|
+
const location = requireCanonicalArtifactLocation(changeId, outputPath);
|
|
6376
|
+
const key = `${location.changeId}:${location.outputPath}`;
|
|
5406
6377
|
if (!this._globArtifactFiles.has(key)) this._globArtifactFiles.set(key, new ReactiveState([]));
|
|
5407
|
-
await this.startStreamOnce(`change:${changeId}:glob:${outputPath}`, this._globArtifactFiles.get(key), () => readGlobArtifactFiles(this.projectDir, changeId, outputPath), this.controller.signal);
|
|
6378
|
+
await this.startStreamOnce(`change:${location.changeId}:glob:${location.outputPath}`, this._globArtifactFiles.get(key), () => readGlobArtifactFiles(this.projectDir, location.changeId, location.outputPath), this.controller.signal);
|
|
5408
6379
|
}
|
|
5409
6380
|
async ensureSchemaResolution(name) {
|
|
5410
6381
|
if (!this._schemaResolutions.has(name)) this._schemaResolutions.set(name, new ReactiveState(null));
|
|
@@ -5433,8 +6404,9 @@ var OpsxKernel = class {
|
|
|
5433
6404
|
await this.startStreamOnce(`schema:${key}:template-contents`, this._templateContents.get(key), () => this.fetchTemplateContents(schema), this.controller.signal);
|
|
5434
6405
|
}
|
|
5435
6406
|
async ensureChangeMetadata(changeId) {
|
|
5436
|
-
|
|
5437
|
-
|
|
6407
|
+
const canonicalChangeId = requireCanonicalOpenSpecEntityId(changeId, "changeId");
|
|
6408
|
+
if (!this._changeMetadata.has(canonicalChangeId)) this._changeMetadata.set(canonicalChangeId, new ReactiveState(null));
|
|
6409
|
+
await this.startStreamOnce(`change:${canonicalChangeId}:metadata`, this._changeMetadata.get(canonicalChangeId), () => this.fetchChangeMetadata(canonicalChangeId), this.controller.signal);
|
|
5438
6410
|
}
|
|
5439
6411
|
combineSignals(a, b) {
|
|
5440
6412
|
const ctrl = new AbortController();
|
|
@@ -5454,12 +6426,23 @@ function parseSchemaYamlInline(content, fallbackName, path) {
|
|
|
5454
6426
|
|
|
5455
6427
|
//#endregion
|
|
5456
6428
|
//#region src/pty-protocol.ts
|
|
6429
|
+
/**
|
|
6430
|
+
* Orthogonal intents (updated 2026-07-22 Asia/Shanghai):
|
|
6431
|
+
* 1. Define the type-safe PTY client/server transport protocol.
|
|
6432
|
+
* 2. Preserve explicit launch-project or planning-root cwd identity across create/list/reconnect.
|
|
6433
|
+
* 3. Carry terminal output, control metadata, lifecycle, and failure messages.
|
|
6434
|
+
* 4. Carry opaque planning-root generation evidence through PTY creation and guarded workflow input.
|
|
6435
|
+
*
|
|
6436
|
+
* Original request (2026-07-16): "3.8 Terminal exposes explicit launch-project cwd and planning-root cwd while preserving inherited XDG_DATA_HOME"
|
|
6437
|
+
* Owner-reported defect (2026-07-21): Same-generation Agent terminals are unavailable to Send.
|
|
6438
|
+
*/
|
|
5457
6439
|
const PositiveInt = z.number().int().positive();
|
|
5458
6440
|
const PtyPlatformSchema = z.enum([
|
|
5459
6441
|
"windows",
|
|
5460
6442
|
"macos",
|
|
5461
6443
|
"common"
|
|
5462
6444
|
]);
|
|
6445
|
+
const TerminalCwdTargetSchema = z.enum(["launch-project", "planning-root"]);
|
|
5463
6446
|
const CloseCallbackUrlSchema = z.union([z.string(), z.record(z.string())]);
|
|
5464
6447
|
const PtySessionInfoSchema = z.object({
|
|
5465
6448
|
id: z.string().min(1),
|
|
@@ -5470,7 +6453,10 @@ const PtySessionInfoSchema = z.object({
|
|
|
5470
6453
|
isExited: z.boolean(),
|
|
5471
6454
|
exitCode: z.number().int().nullable(),
|
|
5472
6455
|
closeTip: z.string().optional(),
|
|
5473
|
-
closeCallbackUrl: CloseCallbackUrlSchema.optional()
|
|
6456
|
+
closeCallbackUrl: CloseCallbackUrlSchema.optional(),
|
|
6457
|
+
cwdTarget: TerminalCwdTargetSchema,
|
|
6458
|
+
initialCwd: z.string().min(1),
|
|
6459
|
+
rootGeneration: z.string().min(1).nullable()
|
|
5474
6460
|
});
|
|
5475
6461
|
const PtyCreateMessageSchema = z.object({
|
|
5476
6462
|
type: z.literal("create"),
|
|
@@ -5479,6 +6465,8 @@ const PtyCreateMessageSchema = z.object({
|
|
|
5479
6465
|
rows: PositiveInt.optional(),
|
|
5480
6466
|
command: z.string().min(1).optional(),
|
|
5481
6467
|
args: z.array(z.string()).optional(),
|
|
6468
|
+
cwdTarget: TerminalCwdTargetSchema,
|
|
6469
|
+
expectedRootGeneration: z.string().min(1).optional(),
|
|
5482
6470
|
closeTip: z.string().optional(),
|
|
5483
6471
|
closeCallbackUrl: CloseCallbackUrlSchema.optional()
|
|
5484
6472
|
});
|
|
@@ -5487,6 +6475,14 @@ const PtyInputMessageSchema = z.object({
|
|
|
5487
6475
|
sessionId: z.string().min(1),
|
|
5488
6476
|
data: z.string()
|
|
5489
6477
|
});
|
|
6478
|
+
/** A workflow payload whose terminal and current Planning-root generation require Server approval. */
|
|
6479
|
+
const PtyWorkflowInputMessageSchema = z.object({
|
|
6480
|
+
type: z.literal("workflow-input"),
|
|
6481
|
+
requestId: z.string().min(1),
|
|
6482
|
+
sessionId: z.string().min(1),
|
|
6483
|
+
expectedRootGeneration: z.string().min(1),
|
|
6484
|
+
data: z.string()
|
|
6485
|
+
});
|
|
5490
6486
|
const PtyResizeMessageSchema = z.object({
|
|
5491
6487
|
type: z.literal("resize"),
|
|
5492
6488
|
sessionId: z.string().min(1),
|
|
@@ -5507,6 +6503,7 @@ const PtyListMessageSchema = z.object({ type: z.literal("list") });
|
|
|
5507
6503
|
const PtyClientMessageSchema = z.discriminatedUnion("type", [
|
|
5508
6504
|
PtyCreateMessageSchema,
|
|
5509
6505
|
PtyInputMessageSchema,
|
|
6506
|
+
PtyWorkflowInputMessageSchema,
|
|
5510
6507
|
PtyResizeMessageSchema,
|
|
5511
6508
|
PtyCloseMessageSchema,
|
|
5512
6509
|
PtyAttachMessageSchema,
|
|
@@ -5516,7 +6513,23 @@ const PtyCreatedResponseSchema = z.object({
|
|
|
5516
6513
|
type: z.literal("created"),
|
|
5517
6514
|
requestId: z.string().min(1),
|
|
5518
6515
|
sessionId: z.string().min(1),
|
|
5519
|
-
platform: PtyPlatformSchema
|
|
6516
|
+
platform: PtyPlatformSchema,
|
|
6517
|
+
cwdTarget: TerminalCwdTargetSchema,
|
|
6518
|
+
initialCwd: z.string().min(1),
|
|
6519
|
+
rootGeneration: z.string().min(1).nullable()
|
|
6520
|
+
});
|
|
6521
|
+
/** Confirms that guarded workflow input reached the Server-owned PTY session. */
|
|
6522
|
+
const PtyWorkflowInputAcceptedResponseSchema = z.object({
|
|
6523
|
+
type: z.literal("workflow-input-accepted"),
|
|
6524
|
+
requestId: z.string().min(1),
|
|
6525
|
+
sessionId: z.string().min(1)
|
|
6526
|
+
});
|
|
6527
|
+
/** Rejects guarded workflow input before PTY write while preserving the Server-owned reason. */
|
|
6528
|
+
const PtyWorkflowInputRejectedResponseSchema = z.object({
|
|
6529
|
+
type: z.literal("workflow-input-rejected"),
|
|
6530
|
+
requestId: z.string().min(1),
|
|
6531
|
+
sessionId: z.string().min(1),
|
|
6532
|
+
message: z.string().min(1)
|
|
5520
6533
|
});
|
|
5521
6534
|
const PtyOutputResponseSchema = z.object({
|
|
5522
6535
|
type: z.literal("output"),
|
|
@@ -5585,7 +6598,8 @@ const PtyErrorCodeSchema = z.enum([
|
|
|
5585
6598
|
"INVALID_JSON",
|
|
5586
6599
|
"INVALID_MESSAGE",
|
|
5587
6600
|
"SESSION_NOT_FOUND",
|
|
5588
|
-
"PTY_CREATE_FAILED"
|
|
6601
|
+
"PTY_CREATE_FAILED",
|
|
6602
|
+
"UNAUTHORIZED"
|
|
5589
6603
|
]);
|
|
5590
6604
|
const PtyErrorResponseSchema = z.object({
|
|
5591
6605
|
type: z.literal("error"),
|
|
@@ -5595,6 +6609,8 @@ const PtyErrorResponseSchema = z.object({
|
|
|
5595
6609
|
});
|
|
5596
6610
|
const PtyServerMessageSchema = z.discriminatedUnion("type", [
|
|
5597
6611
|
PtyCreatedResponseSchema,
|
|
6612
|
+
PtyWorkflowInputAcceptedResponseSchema,
|
|
6613
|
+
PtyWorkflowInputRejectedResponseSchema,
|
|
5598
6614
|
PtyOutputResponseSchema,
|
|
5599
6615
|
PtyExitResponseSchema,
|
|
5600
6616
|
PtyTitleResponseSchema,
|
|
@@ -5609,4 +6625,4 @@ const PtyServerMessageSchema = z.discriminatedUnion("type", [
|
|
|
5609
6625
|
]);
|
|
5610
6626
|
|
|
5611
6627
|
//#endregion
|
|
5612
|
-
export { AI_TOOLS, ApplyInstructionsContextFilesSchema, ApplyInstructionsSchema, ApplyTaskSchema, ArtifactInstructionsSchema, ArtifactStatusSchema, BUILTIN_SOUND_IDS, BUILTIN_SOUND_OPTIONS, BUILTIN_TERMINAL_SPAWN_COMMANDS, BatchTranslateEventSchema, BatchTranslateInputSchema, BuiltinSoundIdSchema, CODE_EDITOR_THEME_VALUES, CUSTOM_SOUND_ADD_VALUE, ChangeFileSchema, ChangeSchema, ChangeStatusSchema, CliExecutor, CodeEditorThemeSchema, ConfigManager, CustomSoundHashSchema, CustomSoundIdSchema, CustomSoundMetadataFileSchema, CustomSoundMetadataSchema, DASHBOARD_METRIC_KEYS, DEFAULT_BELL_SOUND_ID, DEFAULT_CONFIG, DEFAULT_GIT_DIFF_EAGER_LINE_BUDGET, DEFAULT_GLOBAL_SETTINGS, DEFAULT_NOTIFICATION_SOUND_ID, DEFAULT_TERMINAL_DARK_THEME, DEFAULT_TERMINAL_LIGHT_THEME, DEFAULT_TERMINAL_THEME_MODE, DEFAULT_TRANSLATION_CACHE_ENTRY_LIMIT, DEFAULT_TRANSLATION_ENGINE_ID, DOCUMENT_TRANSLATION_DISPLAY_MODES, DashboardConfigSchema, DeltaOperationType, DeltaSchema, DeltaSpecSchema, DependencyInfoSchema, DocumentTranslationConfigSchema, DocumentTranslationConfigUpdateSchema, DocumentTranslationDisplayModeSchema, DocumentTranslationGlobalSettingsUpdateSchema, FILE_PREVIEW_KINDS, GitConfigSchema, GlobalSettingsManager, HOSTED_SHELL_PROTOCOL_VERSION, LEGACY_SOUND_ID_MAP, LOCAL_MODEL_PROFILE_DEFINITIONS, LocalModelAssetLogSchema, LocalModelAssetPlanSnapshotSchema, LocalModelAssetStateSchema, LocalModelDownloadStatusSchema, LocalModelLifecycleFileStateSchema, LocalModelLifecycleGroupStateSchema, LocalModelProfileLoadStateSchema, LocalModelProfileManifestFileSchema, LocalModelProfileManifestGroupSchema, LocalModelProfileManifestSchema, LocalModelProfileStatusSchema, MAX_TRANSLATION_CACHE_ENTRY_LIMIT, MIN_TRANSLATION_CACHE_ENTRY_LIMIT, ManagedLocalCatalogSourceSchema, MarkdownParser, MarkdownReadingPluginRegistry, NOTIFICATION_SOUND_OPTIONS, NOTIFICATION_SOUND_VALUES, NotificationActionSchema, NotificationGroupKeySchema, NotificationPublishInputSchema, NotificationRecordSchema, NotificationSettingsSchema, NotificationSoundSchema, NotificationSourceSchema, OFFICIAL_APP_BASE_URL, OPENSPECUI_HOOKS_VERSION, OPENSPECUI_RUNTIME_CAPABILITIES, OPEN_SPEC_READING_SECTIONS_PROJECTION_ID, OPEN_SPEC_SPEC_PROJECTION_ID, OPSX_AGENT_INVOCATION_MODE_VALUES, OpenSpecAdapter, OpenSpecUIConfigSchema, OpenSpecUIGlobalSettingsSchema, OpenSpecUIGlobalSettingsUpdateSchema, OpenSpecWatcher, OpsxAgentInvocationModeSchema, OpsxConfigSchema, OpsxKernel, ProjectWatcher, PtyAttachMessageSchema, PtyBellResponseSchema, PtyBufferResponseSchema, PtyClientMessageSchema, PtyCloseMessageSchema, PtyCreateMessageSchema, PtyCreatedResponseSchema, PtyErrorCodeSchema, PtyErrorResponseSchema, PtyExitResponseSchema, PtyInputMessageSchema, PtyListMessageSchema, PtyListResponseSchema, PtyOutputResponseSchema, PtyPlatformSchema, PtyProcessTitleResponseSchema, PtyResizeMessageSchema, PtyServerMessageSchema, PtyTitleResponseSchema, ReactiveContext, ReactiveState, RequirementSchema, SERVICE_TRANSLATION_ENGINE_IDS, SILENT_SOUND_ID, ScenarioStepKeywordSchema, ScenarioStepSchema, SchemaArtifactSchema, SchemaDetailSchema, SchemaInfoSchema, SchemaResolutionSchema, ServiceTranslationEngineIdSchema, SoundConfigIdSchema, SoundIdSchema, SpecSchema, StoreDoctorResultSchema, StoreListResultSchema, TERMINAL_BELL_SOUND_OPTIONS, TERMINAL_BELL_SOUND_VALUES, TERMINAL_COMMAND_FIELD_TYPE_VALUES, TERMINAL_SHELL_QUOTE_STYLE_VALUES, TERMINAL_THEME_MODE_VALUES, TERMINAL_THEME_VALUES, TOOL_WORKFLOW_TO_SKILL_DIR, TRANSLATION_CACHE_POLICY_VERSION, TRANSLATION_ENGINE_IDS, TRANSLATION_ENGINE_MANIFESTS, TRANSLATOR_CONTRACT_VERSION, TaskSchema, TemplatesSchema, TerminalBellSoundSchema, TerminalCommandFieldSchema, TerminalConfigSchema, TerminalControlParser, TerminalInvocationSettingsSchema, TerminalNotificationParser, TerminalRendererEngineSchema, TerminalShellProfileSchema, TerminalShellQuoteStyleSchema, TerminalSpawnCommandSchema, TerminalThemeModeSchema, TerminalThemeSchema, TranslationCacheEntrySchema, TranslationCacheReadInputSchema, TranslationCacheSettingsSchema, TranslationCacheSettingsUpdateSchema, TranslationCacheStatsSchema, TranslationCacheWriteInputSchema, TranslationDownloadFilePlanSchema, TranslationDownloadGroupPlanSchema, TranslationEngineAssetStateSchema, TranslationEngineAssetStatusSchema, TranslationEngineDependencyStateSchema, TranslationEngineDependencyStatusSchema, TranslationEngineGlobalSettingsSchema, TranslationEngineGlobalSettingsUpdateSchema, TranslationEngineIdSchema, TranslationEngineInstallLogEventSchema, TranslationEngineInstallLogStreamSchema, TranslationEngineLifecycleEventSchema, TranslationEngineLifecycleStatusSchema, TranslationEngineProjectSettingsSchema, TranslationEngineProjectSettingsUpdateSchema, TranslationEngineRuntimeStateSchema, TranslationEngineRuntimeStatusSchema, TranslationLocalCt2SettingsSchema, TranslationLocalCt2SettingsUpdateSchema, TranslationLocalLlamaSettingsSchema, TranslationLocalLlamaSettingsUpdateSchema, TranslationLocalSettingsSchema, TranslationLocalSettingsUpdateSchema, TranslationOpenAISettingsSchema, TranslationOpenAISettingsUpdateSchema, VIRTUAL_PROJECT_DIRNAME, Validator, acquireWatcher, annotateOpenSpecFacts, annotateOpenSpecMarkdown, buildBackendHealthPayload, buildCliRunnerCandidates, buildEmbeddedUiLaunchUrl, buildHostedLaunchUrl, buildLocalDownloadPlanFromRepositoryFiles, buildLocalDownloadPlanFromRuntimeProfileFiles, buildMarkdownParentMap, buildOpsxEntityDetail, buildRuntimePackageInstallCommand, builtinOpenSpecReadingPlugin, checkLocalDirectionalModelLanguagePair, classifyStoreCliOutput, clearCache, closeAllProjectWatchers, closeAllWatchers, computeTaskProgress, contextStorage, createCleanCliEnv, createFileChangeObservable, createMarkdownReadingDocument, createMarkdownReadingDocumentFromFacts, createOpenSpecReadingPlugin, createTranslationEngineLifecycleStatus, customHashFromSoundId, detectRuntimePackageManager, getActiveWatcherCount, getAllToolIds, getAllTools, getAvailableToolIds, getAvailableTools, getBuiltinSoundUrl, getCacheSize, getConfiguredTools, getDefaultCliCommand, getDefaultCliCommandString, getDefaultGlobalSettingsPath, getDetectedProjectTools, getManagedLocalTranslationEngineManifest, getMarkdownAnnotation, getMarkdownAnnotationsForFact, getMarkdownFactSpan, getMarkdownHeadingEnd, getMarkdownHeadingFacts, getNotificationGroupKey, getNotificationGroupLabel, getOpenSpecAnnotation, getOpenSpecAnnotationsForFact, getOpenSpecProjectionAnnotation, getOpenSpecReadingSections, getOpsxEntityMetadataPath, getOpsxEntityRootRelativePath, getProjectWatcher, getTerminalCommandDefaultValues, getToolById, getToolInitStates, getTranslationEngineLifecycleMessage, getTranslationEngineManifest, getWatchedProjectDir, getWatcherRuntimeStatus, groupNotifications, inferFileMime, inferFilePreviewKind, inferLocalDirectionalModelLanguagePair, initWatcherPool, isBackendHealthRuntimeMetadata, isBatchTranslationAbort, isDirectionalManagedLocalTranslationEngineId, isGlobPattern, isHostedBackendHealthResponse, isManagedLocalTranslationEngineId, isMarkdownTaskSourcePath, isOpsxGlobPattern, isPreviewableFile, isSupportedEmbeddedUiUrl, isTerminalRendererEngine, isTextLikeFile, isTextLikeMime, isToolConfigured, isTranslationEngineDependencyReady, isTranslationEngineRuntimeReady, isWatcherPoolInitialized, normalizeBatchTranslationError, normalizeEmbeddedUiUrl, normalizeHostedAppBaseUrl, normalizeLegacySoundId, normalizeOpsxEntityPath, openSpecAnnotationRules, opsxGlobToRegex, opsxPathMatchesPattern, parseCliCommand, parseMarkdownFacts, parseMarkdownTasks, parseOpenSpecMarkdownToSpec, parseOpsxEntityMetadata, parseOpsxSchemaDetail, projectAnnotatedOpenSpecToSpec, projectOpenSpecMarkdown, projectTasksFromMarkdownFiles, quoteTerminalShellArg, reactiveExists, reactiveReadDir, reactiveReadFile, reactiveStat, renderTerminalCommandArgs, renderTerminalSpawnCommandLine, resolveHostedAppBaseUrl, resolveRuntimePackageInstallStrategy, resolveTerminalShellDefaults, runControlledTranslationTask, selectLocalDownloadGroup, shouldShowTranslationEngineInstallGate, sniffGlobalCli, sortMarkdownReadingPlugins, soundIdFromCustomHash, subscribeWatcherRuntimeStatus, terminalNotificationEventToPublishInput, toMarkdownFactKind, toOpsxDisplayPath, toPersistedGlobalSettings, toStoreFeatureResult, trimMarkdownSlice };
|
|
6628
|
+
export { ACCESS_GATE_CREDENTIAL_BYTES, AI_TOOLS, ApplyInstructionsContextFilesSchema, ApplyInstructionsProjectionSchema, ApplyInstructionsSchema, ApplyTaskSchema, ArtifactInstructionsSchema, ArtifactStatusSchema, BUILTIN_SOUND_IDS, BUILTIN_SOUND_OPTIONS, BUILTIN_TERMINAL_SPAWN_COMMANDS, BatchTranslateEventSchema, BatchTranslateInputSchema, BuiltinSoundIdSchema, CODE_EDITOR_THEME_VALUES, CUSTOM_SOUND_ADD_VALUE, ChangeFileSchema, ChangeSchema, ChangeStatusSchema, CliApplyInstructionsSchema, CliArchiveSchema, CliArtifactInstructionsSchema, CliChangeListSchema, CliContextSchema, CliDiagnosticFailureSchema, CliDiagnosticSchema, CliDoctorReferenceEntrySchema, CliDoctorSchema, CliExecutor, CliJsonValueSchema, CliProjectionCommandError, CliProjectionCommandEvidenceSchema, CliProjectionFailureSchema, CliProjectionInvalidationCauseSchema, CliProjectionNoticeSchema, CliProjectionStateNameSchema, CliReferenceIndexEntrySchema, CliRootSchema, CliRootSourceSchema, CliShowSpecDocumentSchema, CliShowSpecSchema, CliSpecListSchema, CliStoreCleanupSchema, CliStoreDoctorEntrySchema, CliStoreDoctorSchema, CliStoreListSchema, CliStoreMutationSchema, CliStoreSchema, CliStreamTerminationError, CliValidateSchema, CliWorkflowStatusSchema, CodeEditorThemeSchema, ConfigManager, CustomSoundHashSchema, CustomSoundIdSchema, CustomSoundMetadataFileSchema, CustomSoundMetadataSchema, DASHBOARD_METRIC_KEYS, DEFAULT_BELL_SOUND_ID, DEFAULT_CONFIG, DEFAULT_GIT_DIFF_EAGER_LINE_BUDGET, DEFAULT_GLOBAL_SETTINGS, DEFAULT_NOTIFICATION_SOUND_ID, DEFAULT_TERMINAL_DARK_THEME, DEFAULT_TERMINAL_LIGHT_THEME, DEFAULT_TERMINAL_THEME_MODE, DEFAULT_TRANSLATION_CACHE_ENTRY_LIMIT, DEFAULT_TRANSLATION_ENGINE_ID, DOCUMENT_TRANSLATION_DISPLAY_MODES, DashboardConfigSchema, DashboardGitSnapshotSchema, DashboardOverviewSchema, DashboardSummaryIdentitySchema, DashboardSummaryInvalidationCauseSchema, DashboardSummaryInvalidationSchema, DashboardSummaryProjectionSchema, DashboardSummaryProjectionStateSchema, DashboardTrendsProjectionSchema, DeltaOperationType, DeltaSchema, DeltaSpecSchema, DependencyInfoSchema, DocumentTranslationConfigSchema, DocumentTranslationConfigUpdateSchema, DocumentTranslationDisplayModeSchema, DocumentTranslationGlobalSettingsUpdateSchema, EnvironmentGlobalConfigValueSchema, EnvironmentGlobalFileProjectionDataSchema, EnvironmentGlobalFileProjectionStateSchema, EnvironmentGlobalProjectionDataSchema, EnvironmentGlobalProjectionStateSchema, FILE_PREVIEW_KINDS, GitConfigSchema, GlobalSettingsManager, HOSTED_SHELL_PROTOCOL_VERSION, LEGACY_SOUND_ID_MAP, LOCAL_MODEL_PROFILE_DEFINITIONS, LocalModelAssetLogSchema, LocalModelAssetPlanSnapshotSchema, LocalModelAssetStateSchema, LocalModelDownloadStatusSchema, LocalModelLifecycleFileStateSchema, LocalModelLifecycleGroupStateSchema, LocalModelProfileLoadStateSchema, LocalModelProfileManifestFileSchema, LocalModelProfileManifestGroupSchema, LocalModelProfileManifestSchema, LocalModelProfileStatusSchema, MAX_TRANSLATION_CACHE_ENTRY_LIMIT, MIN_TRANSLATION_CACHE_ENTRY_LIMIT, ManagedLocalCatalogSourceSchema, MarkdownParser, MarkdownReadingPluginRegistry, NOTIFICATION_SOUND_OPTIONS, NOTIFICATION_SOUND_VALUES, NotificationActionSchema, NotificationGroupKeySchema, NotificationPublishInputSchema, NotificationRecordSchema, NotificationSettingsSchema, NotificationSoundSchema, NotificationSourceSchema, OFFICIAL_APP_BASE_URL, OPENSPECUI_DOCUMENT_HOOK_VERSION, OPENSPECUI_RUNTIME_CAPABILITIES, OPENSPECUI_WORKFLOW_HOOK_VERSION, OPEN_SPEC_DATA_DIRECTORY_NAME, OPEN_SPEC_DATA_HOME_OBSERVATION_TARGETS, OPEN_SPEC_READING_SECTIONS_PROJECTION_ID, OPEN_SPEC_SPEC_PROJECTION_ID, OPSX_AGENT_INVOCATION_MODE_VALUES, OPSX_ALL_WORKFLOWS, OPSX_ARTIFACT_INPUT_ACTIONS, OPSX_CHANGE_INPUT_ACTIONS, OPSX_COMMAND_CAPABLE_WORKFLOWS, OPSX_CORE_PROFILE_WORKFLOWS, OPSX_TEXT_INPUT_ACTIONS, OPSX_WORKFLOW_LABELS, OPSX_WORKFLOW_TO_SKILL_DIR, OPSX_WORKFLOW_TO_SKILL_DIR as TOOL_WORKFLOW_TO_SKILL_DIR, OpenSpecAdapter, OpenSpecCliContractExecutor, OpenSpecDataHomeObserver, OpenSpecUIConfigSchema, OpenSpecUIGlobalSettingsSchema, OpenSpecUIGlobalSettingsUpdateSchema, OpenSpecWatcher, OpsxAgentInvocationModeSchema, OpsxCliEvidenceSchema, OpsxConfigBundleSchema, OpsxConfigSchema, OpsxKernel, OpsxStatusEvidenceSchema, OwnedSpecIdentitySchema, PlanningCliProjectionDataSchema, PlanningCliProjectionSelectorSchema, PlanningCliProjectionStateSchema, PlanningConfigJsonValueSchema, PlanningConfigReferenceSchema, ProjectBindingUpdateSchema, ProjectWatcher, PtyAttachMessageSchema, PtyBellResponseSchema, PtyBufferResponseSchema, PtyClientMessageSchema, PtyCloseMessageSchema, PtyCreateMessageSchema, PtyCreatedResponseSchema, PtyErrorCodeSchema, PtyErrorResponseSchema, PtyExitResponseSchema, PtyInputMessageSchema, PtyListMessageSchema, PtyListResponseSchema, PtyOutputResponseSchema, PtyPlatformSchema, PtyProcessTitleResponseSchema, PtyResizeMessageSchema, PtyServerMessageSchema, PtyTitleResponseSchema, RUNTIME_INVALIDATION_FACETS, ReactiveContext, ReactiveObservationEnvironment, ReactiveState, ReferencedSpecIdentitySchema, RequirementSchema, RuntimeInvalidationIndex, RuntimeRootInvalidationRegistry, SERVICE_TRANSLATION_ENGINE_IDS, SILENT_SOUND_ID, ScenarioStepKeywordSchema, ScenarioStepSchema, SchemaArtifactSchema, SchemaDetailSchema, SchemaInfoSchema, SchemaResolutionSchema, ServiceTranslationEngineIdSchema, SoundConfigIdSchema, SoundIdSchema, SpecCatalogSchema, SpecCommandEvidenceSchema, SpecDocumentProjectionSchema, SpecIdentitySchema, SpecSchema, StoreDoctorResultSchema, StoreListResultSchema, StoreMutationLifecycleEventSchema, StoreMutationResultSchema, StoreMutationSchema, StoreMutationStartResponseSchema, TERMINAL_BELL_SOUND_OPTIONS, TERMINAL_BELL_SOUND_VALUES, TERMINAL_COMMAND_FIELD_TYPE_VALUES, TERMINAL_SHELL_QUOTE_STYLE_VALUES, TERMINAL_THEME_MODE_VALUES, TERMINAL_THEME_VALUES, TRANSLATION_CACHE_POLICY_VERSION, TRANSLATION_ENGINE_IDS, TRANSLATION_ENGINE_MANIFESTS, TRANSLATOR_CONTRACT_VERSION, TaskSchema, TemplateContentMapSchema, TemplateContentSchema, TemplatesSchema, TerminalBellSoundSchema, TerminalCommandFieldSchema, TerminalConfigSchema, TerminalControlParser, TerminalCwdTargetSchema, TerminalInvocationSettingsSchema, TerminalNotificationParser, TerminalRendererEngineSchema, TerminalShellProfileSchema, TerminalShellQuoteStyleSchema, TerminalSpawnCommandSchema, TerminalThemeModeSchema, TerminalThemeSchema, TrackedTaskProgressSchema, TranslationCacheEntrySchema, TranslationCacheReadInputSchema, TranslationCacheSettingsSchema, TranslationCacheSettingsUpdateSchema, TranslationCacheStatsSchema, TranslationCacheWriteInputSchema, TranslationDownloadFilePlanSchema, TranslationDownloadGroupPlanSchema, TranslationEngineAssetStateSchema, TranslationEngineAssetStatusSchema, TranslationEngineDependencyStateSchema, TranslationEngineDependencyStatusSchema, TranslationEngineGlobalSettingsSchema, TranslationEngineGlobalSettingsUpdateSchema, TranslationEngineIdSchema, TranslationEngineInstallLogEventSchema, TranslationEngineInstallLogStreamSchema, TranslationEngineLifecycleEventSchema, TranslationEngineLifecycleStatusSchema, TranslationEngineProjectSettingsSchema, TranslationEngineProjectSettingsUpdateSchema, TranslationEngineRuntimeStateSchema, TranslationEngineRuntimeStatusSchema, TranslationLocalCt2SettingsSchema, TranslationLocalCt2SettingsUpdateSchema, TranslationLocalLlamaSettingsSchema, TranslationLocalLlamaSettingsUpdateSchema, TranslationLocalSettingsSchema, TranslationLocalSettingsUpdateSchema, TranslationOpenAISettingsSchema, TranslationOpenAISettingsUpdateSchema, VIRTUAL_PROJECT_DIRNAME, Validator, accessGateFingerprint, acquireWatcher, acquireWatcherRoot, annotateOpenSpecFacts, annotateOpenSpecMarkdown, asEnvUri, buildBackendHealthPayload, buildCliRunnerCandidates, buildEmbeddedUiLaunchUrl, buildHostedLaunchUrl, buildLocalDownloadPlanFromRepositoryFiles, buildLocalDownloadPlanFromRuntimeProfileFiles, buildMarkdownParentMap, buildOpsxEntityDetail, buildRuntimePackageInstallCommand, buildSpecCatalog, builtinOpenSpecReadingPlugin, checkLocalDirectionalModelLanguagePair, classifyStoreCliResult, clearCache, cliProjectionStates, closeAllProjectWatchers, closeAllWatchers, computeEnvUri, constantTimeEqual, contextStorage, createApplyInstructionProgress, createCleanCliEnv, createCliProjectionStateSchema, createDocumentChecklistSummary, createFileChangeObservable, createMarkdownReadingDocument, createMarkdownReadingDocumentFromFacts, createOpenSpecReadingPlugin, createPhysicalReactiveDirectory, createStaticSpecCatalogOwnedProjection, createStaticSpecCatalogReferenceProjection, createStaticSpecCatalogReferenceSource, createToolInitStateProjection, createTrackedTaskProgress, createTranslationEngineLifecycleStatus, customHashFromSoundId, deriveTrackedTaskPhase, detectRuntimePackageManager, generateAccessGateCredential, getActiveWatcherCount, getAllToolIds, getAllTools, getAvailableToolIds, getAvailableTools, getBuiltinSoundUrl, getCacheSize, getConfiguredTools, getDefaultCliCommand, getDefaultCliCommandString, getDefaultGlobalSettingsPath, getDetectedProjectTools, getExternalCodexCommandObservationRoot, getManagedLocalTranslationEngineManifest, getMarkdownAnnotation, getMarkdownAnnotationsForFact, getMarkdownFactSpan, getMarkdownHeadingEnd, getMarkdownHeadingFacts, getNotificationGroupKey, getNotificationGroupLabel, getOpenSpecAnnotation, getOpenSpecAnnotationsForFact, getOpenSpecProjectionAnnotation, getOpenSpecReadingSections, getOpsxEntityMetadataPath, getOpsxEntityRootRelativePath, getProjectWatcher, getRootContextCliSelector, getSpecCatalogEntry, getTerminalCommandDefaultValues, getToolById, getToolInitStates, getTranslationEngineLifecycleMessage, getTranslationEngineManifest, getWatcherRuntimeStatus, groupNotifications, hasCapability, inferFileMime, inferFilePreviewKind, inferLocalDirectionalModelLanguagePair, inspectProjectBinding, isBackendHealthRuntimeMetadata, isBatchTranslationAbort, isDirectionalManagedLocalTranslationEngineId, isGlobPattern, isHostedBackendHealthResponse, isManagedLocalTranslationEngineId, isMarkdownTaskSourcePath, isOpsxCoreWorkflowSelection, isOpsxGlobPattern, isPreviewableFile, isReferencedSpecIdentity, isSupportedEmbeddedUiUrl, isTerminalMutationStatus, isTerminalRendererEngine, isTextLikeFile, isTextLikeMime, isToolConfigured, isTranslationEngineDependencyReady, isTranslationEngineRuntimeReady, isWatcherPoolInitialized, mergeSpecCatalog, normalizeAccessGatePassword, normalizeBatchTranslationError, normalizeEmbeddedUiUrl, normalizeHostedAppBaseUrl, normalizeLegacySoundId, normalizeOpsxEntityPath, openSpecAnnotationRules, opsxGlobToRegex, opsxPathMatchesPattern, parseCliCommand, parseCliCommandResult, parseMarkdownFacts, parseMarkdownTasks, parseOpenSpecMarkdownToSpec, parseOpsxEntityMetadata, parseOpsxSchemaDetail, projectAnnotatedOpenSpecToSpec, projectOpenSpecMarkdown, projectTaskProjectionsFromMarkdownFiles, quoteTerminalShellArg, reactiveExists, reactiveReadDir, reactiveReadFile, reactiveStat, redactSnapshotForPublication, removePhysicalReactivePath, renderTerminalCommandArgs, renderTerminalSpawnCommandLine, requireCanonicalOpenSpecEntityId, requireOpenSpecEntityRelativePath, resolveHostedAppBaseUrl, resolveOpenSpecDataScope, resolveRootContext, resolveRuntimePackageInstallStrategy, resolveTerminalShellDefaults, runControlledTranslationTask, runPhysicalReactivePathMutation, selectLocalDownloadGroup, shouldShowTranslationEngineInstallGate, snapshotHasAbsolutePath, sniffGlobalCli, sortMarkdownReadingPlugins, soundIdFromCustomHash, specIdentityFromRoute, specIdentityKey, specRoutePath, subscribeWatcherRuntimeStatus, terminalNotificationEventToPublishInput, toCliProjectionCommandEvidence, toCliProjectionFailure, toCliProjectionNotice, toMarkdownFactKind, toOpsxDisplayPath, toPersistedGlobalSettings, toStoreFeatureResult, toggleMarkdownTask, trimMarkdownSlice, updateProjectBindingContent, updateReactiveFileCache, writePhysicalReactiveFile };
|