@niujiajie/freespec 0.0.0-stage → 1.15.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/LICENSE +22 -0
- package/README.md +58 -2
- package/bin/freespec.js +5 -0
- package/dist/cli/index.d.ts +25 -0
- package/dist/cli/index.js +359 -0
- package/dist/commands/shared-output.d.ts +18 -0
- package/dist/commands/shared-output.js +61 -0
- package/dist/commands/workflow/default-schema.d.ts +2 -0
- package/dist/commands/workflow/default-schema.js +4 -0
- package/dist/commands/workflow/index.d.ts +13 -0
- package/dist/commands/workflow/index.js +10 -0
- package/dist/commands/workflow/instructions.d.ts +39 -0
- package/dist/commands/workflow/instructions.js +565 -0
- package/dist/commands/workflow/new-change.d.ts +19 -0
- package/dist/commands/workflow/new-change.js +135 -0
- package/dist/commands/workflow/shared.d.ts +94 -0
- package/dist/commands/workflow/shared.js +169 -0
- package/dist/commands/workflow/status.d.ts +18 -0
- package/dist/commands/workflow/status.js +201 -0
- package/dist/core/artifact-graph/graph.d.ts +72 -0
- package/dist/core/artifact-graph/graph.js +165 -0
- package/dist/core/artifact-graph/index.d.ts +9 -0
- package/dist/core/artifact-graph/index.js +14 -0
- package/dist/core/artifact-graph/instruction-loader.d.ts +208 -0
- package/dist/core/artifact-graph/instruction-loader.js +268 -0
- package/dist/core/artifact-graph/outputs.d.ts +19 -0
- package/dist/core/artifact-graph/outputs.js +351 -0
- package/dist/core/artifact-graph/resolver.d.ts +89 -0
- package/dist/core/artifact-graph/resolver.js +342 -0
- package/dist/core/artifact-graph/schema.d.ts +27 -0
- package/dist/core/artifact-graph/schema.js +160 -0
- package/dist/core/artifact-graph/state.d.ts +12 -0
- package/dist/core/artifact-graph/state.js +31 -0
- package/dist/core/artifact-graph/types.d.ts +40 -0
- package/dist/core/artifact-graph/types.js +59 -0
- package/dist/core/available-tools.d.ts +18 -0
- package/dist/core/available-tools.js +49 -0
- package/dist/core/change-layout.d.ts +17 -0
- package/dist/core/change-layout.js +30 -0
- package/dist/core/change-metadata/index.d.ts +2 -0
- package/dist/core/change-metadata/index.js +2 -0
- package/dist/core/change-metadata/schema.d.ts +23 -0
- package/dist/core/change-metadata/schema.js +46 -0
- package/dist/core/change-status-policy.d.ts +49 -0
- package/dist/core/change-status-policy.js +50 -0
- package/dist/core/command-generation/adapters/amazon-q.d.ts +17 -0
- package/dist/core/command-generation/adapters/amazon-q.js +32 -0
- package/dist/core/command-generation/adapters/antigravity.d.ts +13 -0
- package/dist/core/command-generation/adapters/antigravity.js +27 -0
- package/dist/core/command-generation/adapters/atomcode.d.ts +24 -0
- package/dist/core/command-generation/adapters/atomcode.js +46 -0
- package/dist/core/command-generation/adapters/auggie.d.ts +13 -0
- package/dist/core/command-generation/adapters/auggie.js +28 -0
- package/dist/core/command-generation/adapters/bob.d.ts +18 -0
- package/dist/core/command-generation/adapters/bob.js +33 -0
- package/dist/core/command-generation/adapters/claude.d.ts +13 -0
- package/dist/core/command-generation/adapters/claude.js +32 -0
- package/dist/core/command-generation/adapters/cline.d.ts +14 -0
- package/dist/core/command-generation/adapters/cline.js +27 -0
- package/dist/core/command-generation/adapters/codeassistant.d.ts +15 -0
- package/dist/core/command-generation/adapters/codeassistant.js +29 -0
- package/dist/core/command-generation/adapters/codebuddy.d.ts +13 -0
- package/dist/core/command-generation/adapters/codebuddy.js +29 -0
- package/dist/core/command-generation/adapters/codestudio.d.ts +13 -0
- package/dist/core/command-generation/adapters/codestudio.js +27 -0
- package/dist/core/command-generation/adapters/command-code.d.ts +19 -0
- package/dist/core/command-generation/adapters/command-code.js +34 -0
- package/dist/core/command-generation/adapters/continue.d.ts +13 -0
- package/dist/core/command-generation/adapters/continue.js +33 -0
- package/dist/core/command-generation/adapters/costrict.d.ts +13 -0
- package/dist/core/command-generation/adapters/costrict.js +28 -0
- package/dist/core/command-generation/adapters/crush.d.ts +13 -0
- package/dist/core/command-generation/adapters/crush.js +30 -0
- package/dist/core/command-generation/adapters/cursor.d.ts +14 -0
- package/dist/core/command-generation/adapters/cursor.js +31 -0
- package/dist/core/command-generation/adapters/devin.d.ts +19 -0
- package/dist/core/command-generation/adapters/devin.js +36 -0
- package/dist/core/command-generation/adapters/easycode.d.ts +17 -0
- package/dist/core/command-generation/adapters/easycode.js +28 -0
- package/dist/core/command-generation/adapters/factory.d.ts +13 -0
- package/dist/core/command-generation/adapters/factory.js +28 -0
- package/dist/core/command-generation/adapters/gemini.d.ts +13 -0
- package/dist/core/command-generation/adapters/gemini.js +27 -0
- package/dist/core/command-generation/adapters/gigacode.d.ts +17 -0
- package/dist/core/command-generation/adapters/gigacode.js +31 -0
- package/dist/core/command-generation/adapters/github-copilot.d.ts +13 -0
- package/dist/core/command-generation/adapters/github-copilot.js +27 -0
- package/dist/core/command-generation/adapters/iflow.d.ts +13 -0
- package/dist/core/command-generation/adapters/iflow.js +30 -0
- package/dist/core/command-generation/adapters/index.d.ts +40 -0
- package/dist/core/command-generation/adapters/index.js +40 -0
- package/dist/core/command-generation/adapters/junie.d.ts +13 -0
- package/dist/core/command-generation/adapters/junie.js +27 -0
- package/dist/core/command-generation/adapters/kilocode.d.ts +14 -0
- package/dist/core/command-generation/adapters/kilocode.js +23 -0
- package/dist/core/command-generation/adapters/kiro.d.ts +13 -0
- package/dist/core/command-generation/adapters/kiro.js +27 -0
- package/dist/core/command-generation/adapters/lingma.d.ts +13 -0
- package/dist/core/command-generation/adapters/lingma.js +30 -0
- package/dist/core/command-generation/adapters/oh-my-pi.d.ts +21 -0
- package/dist/core/command-generation/adapters/oh-my-pi.js +42 -0
- package/dist/core/command-generation/adapters/opencode.d.ts +14 -0
- package/dist/core/command-generation/adapters/opencode.js +38 -0
- package/dist/core/command-generation/adapters/pi.d.ts +18 -0
- package/dist/core/command-generation/adapters/pi.js +39 -0
- package/dist/core/command-generation/adapters/qoder.d.ts +13 -0
- package/dist/core/command-generation/adapters/qoder.js +30 -0
- package/dist/core/command-generation/adapters/qwen.d.ts +17 -0
- package/dist/core/command-generation/adapters/qwen.js +31 -0
- package/dist/core/command-generation/adapters/roocode.d.ts +14 -0
- package/dist/core/command-generation/adapters/roocode.js +27 -0
- package/dist/core/command-generation/adapters/trae.d.ts +13 -0
- package/dist/core/command-generation/adapters/trae.js +28 -0
- package/dist/core/command-generation/adapters/zcode.d.ts +16 -0
- package/dist/core/command-generation/adapters/zcode.js +33 -0
- package/dist/core/command-generation/generator.d.ts +29 -0
- package/dist/core/command-generation/generator.js +43 -0
- package/dist/core/command-generation/index.d.ts +22 -0
- package/dist/core/command-generation/index.js +24 -0
- package/dist/core/command-generation/invocation.d.ts +73 -0
- package/dist/core/command-generation/invocation.js +77 -0
- package/dist/core/command-generation/registry.d.ts +36 -0
- package/dist/core/command-generation/registry.js +114 -0
- package/dist/core/command-generation/toml.d.ts +19 -0
- package/dist/core/command-generation/toml.js +39 -0
- package/dist/core/command-generation/types.d.ts +63 -0
- package/dist/core/command-generation/types.js +8 -0
- package/dist/core/command-generation/yaml.d.ts +36 -0
- package/dist/core/command-generation/yaml.js +48 -0
- package/dist/core/command-surface.d.ts +15 -0
- package/dist/core/command-surface.js +33 -0
- package/dist/core/config-prompts.d.ts +9 -0
- package/dist/core/config-prompts.js +56 -0
- package/dist/core/config-schema.d.ts +98 -0
- package/dist/core/config-schema.js +308 -0
- package/dist/core/config.d.ts +50 -0
- package/dist/core/config.js +122 -0
- package/dist/core/file-state.d.ts +36 -0
- package/dist/core/file-state.js +144 -0
- package/dist/core/file-view.d.ts +13 -0
- package/dist/core/file-view.js +14 -0
- package/dist/core/global-config.d.ts +101 -0
- package/dist/core/global-config.js +182 -0
- package/dist/core/id.d.ts +17 -0
- package/dist/core/id.js +30 -0
- package/dist/core/index.d.ts +6 -0
- package/dist/core/index.js +7 -0
- package/dist/core/init.d.ts +57 -0
- package/dist/core/init.js +836 -0
- package/dist/core/list.d.ts +11 -0
- package/dist/core/list.js +197 -0
- package/dist/core/migration.d.ts +121 -0
- package/dist/core/migration.js +540 -0
- package/dist/core/onboarding-commands.d.ts +30 -0
- package/dist/core/onboarding-commands.js +38 -0
- package/dist/core/openspec-root.d.ts +47 -0
- package/dist/core/openspec-root.js +196 -0
- package/dist/core/planning-home.d.ts +16 -0
- package/dist/core/planning-home.js +67 -0
- package/dist/core/profile-sync-drift.d.ts +31 -0
- package/dist/core/profile-sync-drift.js +201 -0
- package/dist/core/profiles.d.ts +19 -0
- package/dist/core/profiles.js +33 -0
- package/dist/core/project-config.d.ts +132 -0
- package/dist/core/project-config.js +519 -0
- package/dist/core/references.d.ts +10 -0
- package/dist/core/references.js +46 -0
- package/dist/core/root-selection.d.ts +59 -0
- package/dist/core/root-selection.js +132 -0
- package/dist/core/shared/allowed-tools.d.ts +12 -0
- package/dist/core/shared/allowed-tools.js +12 -0
- package/dist/core/shared/ide-restart.d.ts +26 -0
- package/dist/core/shared/ide-restart.js +42 -0
- package/dist/core/shared/index.d.ts +11 -0
- package/dist/core/shared/index.js +11 -0
- package/dist/core/shared/skill-content-equivalence.d.ts +7 -0
- package/dist/core/shared/skill-content-equivalence.js +45 -0
- package/dist/core/shared/skill-files.d.ts +7 -0
- package/dist/core/shared/skill-files.js +43 -0
- package/dist/core/shared/skill-generation.d.ts +49 -0
- package/dist/core/shared/skill-generation.js +125 -0
- package/dist/core/shared/skill-paths.d.ts +13 -0
- package/dist/core/shared/skill-paths.js +23 -0
- package/dist/core/shared/tool-detection.d.ts +99 -0
- package/dist/core/shared/tool-detection.js +318 -0
- package/dist/core/shared-skill-target.d.ts +51 -0
- package/dist/core/shared-skill-target.js +239 -0
- package/dist/core/store/errors.d.ts +20 -0
- package/dist/core/store/errors.js +22 -0
- package/dist/core/store/index.d.ts +2 -0
- package/dist/core/store/index.js +2 -0
- package/dist/core/styles/palette.d.ts +7 -0
- package/dist/core/styles/palette.js +8 -0
- package/dist/core/templates/optional-workflow.d.ts +69 -0
- package/dist/core/templates/optional-workflow.js +140 -0
- package/dist/core/templates/types.d.ts +19 -0
- package/dist/core/templates/types.js +5 -0
- package/dist/core/templates/workflows/personal.d.ts +22 -0
- package/dist/core/templates/workflows/personal.js +225 -0
- package/dist/core/update-plan.d.ts +127 -0
- package/dist/core/update-plan.js +390 -0
- package/dist/core/update.d.ts +154 -0
- package/dist/core/update.js +723 -0
- package/dist/core/version-check.d.ts +188 -0
- package/dist/core/version-check.js +906 -0
- package/dist/core/zod-issues.d.ts +4 -0
- package/dist/core/zod-issues.js +10 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +3 -0
- package/dist/prompts/searchable-multi-select.d.ts +35 -0
- package/dist/prompts/searchable-multi-select.js +188 -0
- package/dist/telemetry/config.d.ts +37 -0
- package/dist/telemetry/config.js +148 -0
- package/dist/telemetry/index.d.ts +39 -0
- package/dist/telemetry/index.js +217 -0
- package/dist/telemetry/opt-out.d.ts +17 -0
- package/dist/telemetry/opt-out.js +27 -0
- package/dist/ui/ascii-patterns.d.ts +16 -0
- package/dist/ui/ascii-patterns.js +133 -0
- package/dist/ui/welcome-screen.d.ts +18 -0
- package/dist/ui/welcome-screen.js +199 -0
- package/dist/utils/change-metadata.d.ts +116 -0
- package/dist/utils/change-metadata.js +324 -0
- package/dist/utils/change-utils.d.ts +79 -0
- package/dist/utils/change-utils.js +121 -0
- package/dist/utils/ci.d.ts +12 -0
- package/dist/utils/ci.js +16 -0
- package/dist/utils/command-references.d.ts +104 -0
- package/dist/utils/command-references.js +190 -0
- package/dist/utils/date.d.ts +8 -0
- package/dist/utils/date.js +13 -0
- package/dist/utils/executable-path.d.ts +5 -0
- package/dist/utils/executable-path.js +44 -0
- package/dist/utils/file-system.d.ts +58 -0
- package/dist/utils/file-system.js +432 -0
- package/dist/utils/index.d.ts +6 -0
- package/dist/utils/index.js +9 -0
- package/dist/utils/interactive.d.ts +85 -0
- package/dist/utils/interactive.js +174 -0
- package/dist/utils/item-discovery.d.ts +15 -0
- package/dist/utils/item-discovery.js +33 -0
- package/dist/utils/line-endings.d.ts +34 -0
- package/dist/utils/line-endings.js +46 -0
- package/dist/utils/match.d.ts +3 -0
- package/dist/utils/match.js +22 -0
- package/dist/utils/nested-change.d.ts +35 -0
- package/dist/utils/nested-change.js +185 -0
- package/dist/utils/shell-detection.d.ts +20 -0
- package/dist/utils/shell-detection.js +94 -0
- package/dist/utils/task-progress.d.ts +67 -0
- package/dist/utils/task-progress.js +184 -0
- package/package.json +74 -4
- package/schemas//351/273/230/350/256/244/schema.yaml +68 -0
- package/schemas//351/273/230/350/256/244/templates//344/273/273/345/212/241/346/270/205/345/215/225.md +25 -0
- package/schemas//351/273/230/350/256/244/templates//346/265/213/350/257/225/347/224/250/344/276/213.md +34 -0
- package/schemas//351/273/230/350/256/244/templates//350/256/276/350/256/241/346/226/207/346/241/243.md +172 -0
- package/schemas//351/273/230/350/256/244/templates//351/234/200/346/261/202/346/226/207/346/241/243.md +19 -0
- package/skill-files//344/273/273/345/212/241/references//346/212/244/346/240/217.md +43 -0
- package/skill-files//344/273/273/345/212/241/references//347/262/222/345/272/246.md +23 -0
- package/skill-files//344/273/273/345/212/241/references//347/272/246/346/235/237.md +8 -0
- package/skill-files//344/273/273/345/212/241/scripts/check-tasks.mjs +83 -0
- package/skill-files//345/206/231Java/references/Java/350/247/204/350/214/203.md +25 -0
- package/skill-files//345/206/231Java/references//345/215/261/351/231/251/345/206/231/346/263/225.md +27 -0
- package/skill-files//345/206/231Java/references//345/256/214/346/210/220/345/257/271/347/205/247.md +15 -0
- package/skill-files//345/206/231Java/references//346/212/244/346/240/217.md +35 -0
- package/skill-files//345/206/231Java/references//347/272/246/346/235/237.md +8 -0
- package/skill-files//345/206/231Java/references//350/270/251/345/235/221.md +17 -0
- package/skill-files//345/256/241Java/references//346/212/244/346/240/217.md +19 -0
- package/skill-files//345/256/241Java/references//347/244/272/344/276/213.md +23 -0
- package/skill-files//345/256/241Java/references//347/272/246/346/235/237.md +7 -0
- package/skill-files//345/256/241Java/references//350/247/204/345/210/231.md +23 -0
- package/skill-files//345/256/241Java/references//351/242/206/345/237/237/346/243/200/351/241/271.md +58 -0
- package/skill-files//345/256/241/346/226/207/346/241/243/references//346/212/244/346/240/217.md +18 -0
- package/skill-files//345/256/241/346/226/207/346/241/243/references//347/272/246/346/235/237.md +8 -0
- package/skill-files//345/256/241/346/226/207/346/241/243/references//347/273/264/345/272/246.md +17 -0
- package/skill-files//345/256/241/346/226/207/346/241/243/references//350/270/251/345/235/221.md +11 -0
- package/skill-files//346/226/260/345/273/272/references//344/272/244/346/216/245.md +11 -0
- package/skill-files//346/226/260/345/273/272/references//346/212/244/346/240/217.md +14 -0
- package/skill-files//346/226/260/345/273/272/references//347/272/246/346/235/237.md +7 -0
- package/skill-files//346/267/261/346/200/235/config/prompts.yaml +114 -0
- package/skill-files//346/267/261/346/200/235/references//344/270/211/346/226/271/346/240/270/345/257/271.md +72 -0
- package/skill-files//346/267/261/346/200/235/references//344/276/213/345/255/220.md +39 -0
- package/skill-files//346/267/261/346/200/235/references//346/200/216/344/271/210/346/203/263.md +61 -0
- package/skill-files//346/267/261/346/200/235/references//346/212/244/346/240/217.md +26 -0
- package/skill-files//346/267/261/346/200/235/references//347/202/271/351/200/211/346/217/220/351/227/256.md +66 -0
- package/skill-files//346/267/261/346/200/235/references//347/231/273/350/256/260.md +45 -0
- package/skill-files//346/267/261/346/200/235/references//347/272/246/346/235/237.md +9 -0
- package/skill-files//347/224/250/344/276/213/references//346/200/216/344/271/210/345/206/231.md +13 -0
- package/skill-files//347/224/250/344/276/213/references//346/212/244/346/240/217.md +42 -0
- package/skill-files//347/224/250/344/276/213/references//347/272/246/346/235/237.md +5 -0
- package/skill-files//347/224/250/344/276/213/scripts/check-cases.mjs +70 -0
- package/skill-files//350/256/276/350/256/241/references//345/207/206/345/244/207.md +25 -0
- package/skill-files//350/256/276/350/256/241/references//346/212/244/346/240/217.md +134 -0
- package/skill-files//350/256/276/350/256/241/references//346/226/207/351/243/216.md +17 -0
- package/skill-files//350/256/276/350/256/241/references//347/225/214/351/235/242.md +18 -0
- package/skill-files//350/256/276/350/256/241/references//347/261/273/345/236/213.md +19 -0
- package/skill-files//350/256/276/350/256/241/references//347/272/246/346/235/237.md +25 -0
- package/skill-files//350/256/276/350/256/241/scripts/_smoke.mjs +72 -0
- package/skill-files//350/256/276/350/256/241/scripts/check-design.mjs +331 -0
- package/skill-files//351/234/200/346/261/202/references//346/212/244/346/240/217.md +26 -0
- package/skill-files//351/234/200/346/261/202/references//346/226/207/351/243/216.md +23 -0
- package/skill-files//351/234/200/346/261/202/references//346/267/261/346/200/235/346/221/204/345/205/245.md +21 -0
- package/skill-files//351/234/200/346/261/202/references//347/272/246/346/235/237.md +8 -0
- package/skill-files//351/234/200/346/261/202/references//351/234/200/346/261/202/345/276/205/345/256/232.md +13 -0
- package/skill-files//351/234/200/346/261/202/scripts/check-requirements.mjs +60 -0
- package/skill-files//351/252/214/346/224/266/references//346/212/244/346/240/217.md +27 -0
- package/skill-files//351/252/214/346/224/266/references//346/240/270/345/257/271.md +17 -0
- package/skill-files//351/252/214/346/224/266/references//347/272/246/346/235/237.md +7 -0
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Telemetry module for anonymous usage analytics.
|
|
3
|
+
*
|
|
4
|
+
* Privacy-first design:
|
|
5
|
+
* - Only tracks command name and version
|
|
6
|
+
* - No arguments, file paths, or content
|
|
7
|
+
* - Opt-out via OPENSPEC_TELEMETRY=0, DO_NOT_TRACK=1, or
|
|
8
|
+
* `freespec config set telemetry.enabled false`
|
|
9
|
+
* - Auto-disabled in CI environments
|
|
10
|
+
* - Anonymous ID is a random UUID with no relation to the user
|
|
11
|
+
*
|
|
12
|
+
* Events are sent with a plain fetch to PostHog's stable public `/batch/`
|
|
13
|
+
* endpoint — the same one posthog-node used — instead of through the SDK.
|
|
14
|
+
* The SDK's only remaining job here was the wire format: every reliability
|
|
15
|
+
* knob was already forced to "send one event immediately, time-bounded,
|
|
16
|
+
* never retry, never throw". Carrying `posthog-node` for that shipped its
|
|
17
|
+
* fast-moving transitive tree (`@posthog/core`, `@posthog/types`, multiple
|
|
18
|
+
* releases per day) to every downstream consumer, where supply-chain age
|
|
19
|
+
* policies such as pnpm's `minimumReleaseAge` rejected the freshly published
|
|
20
|
+
* versions and broke installs (#1390).
|
|
21
|
+
*/
|
|
22
|
+
import { randomUUID } from 'crypto';
|
|
23
|
+
import { getGlobalConfig, isGlobalConfigUnreadable } from '../core/global-config.js';
|
|
24
|
+
import { isCiEnvironment } from '../utils/ci.js';
|
|
25
|
+
import { getTelemetryConfig, updateTelemetryConfig } from './config.js';
|
|
26
|
+
import { isTelemetryOptedOutByEnv } from './opt-out.js';
|
|
27
|
+
// PostHog API key - public key for client-side analytics
|
|
28
|
+
// This is safe to embed as it only allows sending events, not reading data
|
|
29
|
+
const POSTHOG_API_KEY = 'phc_Hthu8YvaIJ9QaFKyTG4TbVwkbd5ktcAFzVTKeMmoW2g';
|
|
30
|
+
// Using reverse proxy to avoid ad blockers and keep traffic on our domain
|
|
31
|
+
const POSTHOG_HOST = 'https://edge.openspec.dev';
|
|
32
|
+
const TELEMETRY_REQUEST_TIMEOUT_MS = 1000;
|
|
33
|
+
let anonymousId = null;
|
|
34
|
+
/**
|
|
35
|
+
* Requests started by trackCommand and not yet settled, so shutdown can
|
|
36
|
+
* flush them before the process exits. Each request is individually
|
|
37
|
+
* time-bounded, so awaiting them cannot stall exit for more than the
|
|
38
|
+
* request timeout.
|
|
39
|
+
*/
|
|
40
|
+
const pendingEvents = new Set();
|
|
41
|
+
async function safeTelemetryFetch(url, options) {
|
|
42
|
+
try {
|
|
43
|
+
const response = await fetch(url, options);
|
|
44
|
+
// Telemetry never reads the body, but undici keeps the connection
|
|
45
|
+
// occupied until the body is consumed or canceled — dispose of it on
|
|
46
|
+
// every path so no socket outlives shutdown().
|
|
47
|
+
if (response.body) {
|
|
48
|
+
await response.body.cancel();
|
|
49
|
+
}
|
|
50
|
+
if (response.ok) {
|
|
51
|
+
return response;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
// Silent failure - telemetry should never surface network noise
|
|
56
|
+
}
|
|
57
|
+
return new Response(null, { status: 204 });
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Check if telemetry is enabled.
|
|
61
|
+
*
|
|
62
|
+
* Precedence (first match wins):
|
|
63
|
+
* 1. OPENSPEC_TELEMETRY set to anything but an on-value (1/true/yes/on) → disabled
|
|
64
|
+
* 2. DO_NOT_TRACK set to anything but an off-value (0/false/no/off) → disabled
|
|
65
|
+
* 3. CI set to a truthy/on value → disabled (same rule as version-check)
|
|
66
|
+
* 4. global config telemetry.enabled === false → disabled
|
|
67
|
+
* 5. global config file exists but cannot be parsed → disabled
|
|
68
|
+
* 6. otherwise enabled (unset config means on; opt-out model)
|
|
69
|
+
*
|
|
70
|
+
* Kept synchronous so call sites need not become async. Reads config via
|
|
71
|
+
* sync getGlobalConfig() rather than async getTelemetryConfig().
|
|
72
|
+
*/
|
|
73
|
+
export function isTelemetryEnabled() {
|
|
74
|
+
// Explicit opt-out, and the DO_NOT_TRACK standard. Both are read
|
|
75
|
+
// tolerantly (see opt-out.ts): an opt-out that only worked for one exact
|
|
76
|
+
// spelling would leave users tracked who believe they are not.
|
|
77
|
+
if (isTelemetryOptedOutByEnv()) {
|
|
78
|
+
return false;
|
|
79
|
+
}
|
|
80
|
+
// Auto-disable in CI environments (providers use true/1/yes/…)
|
|
81
|
+
if (isCiEnvironment()) {
|
|
82
|
+
return false;
|
|
83
|
+
}
|
|
84
|
+
// Global config opt-out (env/CI remain hard overrides above)
|
|
85
|
+
if (getGlobalConfig().telemetry?.enabled === false) {
|
|
86
|
+
return false;
|
|
87
|
+
}
|
|
88
|
+
// A config file that cannot be parsed reads as defaults, which carry no
|
|
89
|
+
// opt-out, but the file itself may hold one. Unknown is not consent.
|
|
90
|
+
if (isGlobalConfigUnreadable()) {
|
|
91
|
+
return false;
|
|
92
|
+
}
|
|
93
|
+
return true;
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Get or create the anonymous user ID.
|
|
97
|
+
* Lazily generates a UUID on first call and persists it.
|
|
98
|
+
*/
|
|
99
|
+
export async function getOrCreateAnonymousId() {
|
|
100
|
+
// Return cached value if available
|
|
101
|
+
if (anonymousId) {
|
|
102
|
+
return anonymousId;
|
|
103
|
+
}
|
|
104
|
+
// Try to load from config
|
|
105
|
+
const config = await getTelemetryConfig();
|
|
106
|
+
if (config.anonymousId) {
|
|
107
|
+
anonymousId = config.anonymousId;
|
|
108
|
+
return anonymousId;
|
|
109
|
+
}
|
|
110
|
+
// Generate new UUID and persist
|
|
111
|
+
anonymousId = randomUUID();
|
|
112
|
+
await updateTelemetryConfig({ anonymousId });
|
|
113
|
+
return anonymousId;
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Send one capture event to PostHog's batch endpoint. Fire-and-forget:
|
|
117
|
+
* bounded by the request timeout, never throws, never retries.
|
|
118
|
+
*/
|
|
119
|
+
function sendEvent(distinctId, event, properties) {
|
|
120
|
+
const body = JSON.stringify({
|
|
121
|
+
api_key: POSTHOG_API_KEY,
|
|
122
|
+
batch: [
|
|
123
|
+
{
|
|
124
|
+
type: 'capture',
|
|
125
|
+
event,
|
|
126
|
+
distinct_id: distinctId,
|
|
127
|
+
properties,
|
|
128
|
+
timestamp: new Date().toISOString(),
|
|
129
|
+
},
|
|
130
|
+
],
|
|
131
|
+
});
|
|
132
|
+
const request = safeTelemetryFetch(`${POSTHOG_HOST}/batch/`, {
|
|
133
|
+
method: 'POST',
|
|
134
|
+
headers: { 'content-type': 'application/json' },
|
|
135
|
+
body,
|
|
136
|
+
signal: AbortSignal.timeout(TELEMETRY_REQUEST_TIMEOUT_MS),
|
|
137
|
+
}).then(() => undefined);
|
|
138
|
+
pendingEvents.add(request);
|
|
139
|
+
void request.finally(() => pendingEvents.delete(request));
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Track a command execution.
|
|
143
|
+
*
|
|
144
|
+
* @param commandName - The command name (e.g., 'init', 'change:apply')
|
|
145
|
+
* @param version - The FreeSpec version
|
|
146
|
+
*/
|
|
147
|
+
export async function trackCommand(commandName, version) {
|
|
148
|
+
if (!isTelemetryEnabled()) {
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
try {
|
|
152
|
+
// Disclosure before collection. A --json run defers the notice (printing
|
|
153
|
+
// it would corrupt machine-readable output), and for a user whose runs
|
|
154
|
+
// are all --json it may never have appeared — so nothing is sent, and no
|
|
155
|
+
// anonymous id is created, until the notice has actually been shown.
|
|
156
|
+
if (!(await getTelemetryConfig()).noticeSeen) {
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
const userId = await getOrCreateAnonymousId();
|
|
160
|
+
sendEvent(userId, 'command_executed', {
|
|
161
|
+
command: commandName,
|
|
162
|
+
version: version,
|
|
163
|
+
surface: 'cli',
|
|
164
|
+
$ip: null, // Explicitly disable IP tracking
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
catch {
|
|
168
|
+
// Silent failure - telemetry should never break CLI
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* Show first-run telemetry notice if not already seen.
|
|
173
|
+
*/
|
|
174
|
+
export async function maybeShowTelemetryNotice(options = {}) {
|
|
175
|
+
if (!isTelemetryEnabled()) {
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
try {
|
|
179
|
+
const config = await getTelemetryConfig();
|
|
180
|
+
if (config.noticeSeen) {
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
// In --json mode the notice would pollute stdout and break parsers, so
|
|
184
|
+
// defer it: skip the notice AND leave noticeSeen unset so the disclosure
|
|
185
|
+
// still appears on the user's first later non-JSON run.
|
|
186
|
+
if (options.silent) {
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
// Display notice on stderr, not stdout: stdout is reserved for command
|
|
190
|
+
// output (raw passthrough text, JSON, etc.) and must stay parser/pipe-safe.
|
|
191
|
+
console.error('Note: FreeSpec collects anonymous usage stats. Opt out: OPENSPEC_TELEMETRY=0 or freespec config set telemetry.enabled false');
|
|
192
|
+
// Mark as seen
|
|
193
|
+
await updateTelemetryConfig({ noticeSeen: true });
|
|
194
|
+
}
|
|
195
|
+
catch {
|
|
196
|
+
// Silent failure - telemetry should never break CLI
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* Flush pending telemetry events.
|
|
201
|
+
* Call this before CLI exit.
|
|
202
|
+
*/
|
|
203
|
+
export async function shutdown() {
|
|
204
|
+
if (pendingEvents.size === 0) {
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
try {
|
|
208
|
+
await Promise.allSettled([...pendingEvents]);
|
|
209
|
+
}
|
|
210
|
+
catch {
|
|
211
|
+
// Silent failure - telemetry should never break CLI exit
|
|
212
|
+
}
|
|
213
|
+
finally {
|
|
214
|
+
pendingEvents.clear();
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Telemetry opt-out signals, shared by telemetry and the version check so both
|
|
3
|
+
* outbound surfaces honor exactly the same values.
|
|
4
|
+
*
|
|
5
|
+
* Parsing is tolerant in the style of isCiEnvironment(): a user who wrote
|
|
6
|
+
* DO_NOT_TRACK=true meant it, and an exact-match test that silently kept
|
|
7
|
+
* sending would be a privacy control that does not work. Ambiguity fails
|
|
8
|
+
* safe — a value we cannot read as "on" suppresses the request rather than
|
|
9
|
+
* enabling it.
|
|
10
|
+
*/
|
|
11
|
+
/** True when `DO_NOT_TRACK` is set to anything but an explicit off-value. */
|
|
12
|
+
export declare function isDoNotTrackSet(env?: NodeJS.ProcessEnv): boolean;
|
|
13
|
+
/** True when `OPENSPEC_TELEMETRY` is set to anything but an explicit on-value. */
|
|
14
|
+
export declare function isTelemetryDisabledByEnv(env?: NodeJS.ProcessEnv): boolean;
|
|
15
|
+
/** True when either opt-out signal asks us to stay off the network. */
|
|
16
|
+
export declare function isTelemetryOptedOutByEnv(env?: NodeJS.ProcessEnv): boolean;
|
|
17
|
+
//# sourceMappingURL=opt-out.d.ts.map
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Telemetry opt-out signals, shared by telemetry and the version check so both
|
|
3
|
+
* outbound surfaces honor exactly the same values.
|
|
4
|
+
*
|
|
5
|
+
* Parsing is tolerant in the style of isCiEnvironment(): a user who wrote
|
|
6
|
+
* DO_NOT_TRACK=true meant it, and an exact-match test that silently kept
|
|
7
|
+
* sending would be a privacy control that does not work. Ambiguity fails
|
|
8
|
+
* safe — a value we cannot read as "on" suppresses the request rather than
|
|
9
|
+
* enabling it.
|
|
10
|
+
*/
|
|
11
|
+
const ON_VALUES = new Set(['1', 'true', 'yes', 'on']);
|
|
12
|
+
const OFF_VALUES = new Set(['', '0', 'false', 'no', 'off']);
|
|
13
|
+
/** True when `DO_NOT_TRACK` is set to anything but an explicit off-value. */
|
|
14
|
+
export function isDoNotTrackSet(env = process.env) {
|
|
15
|
+
const value = env.DO_NOT_TRACK;
|
|
16
|
+
return value !== undefined && !OFF_VALUES.has(value.trim().toLowerCase());
|
|
17
|
+
}
|
|
18
|
+
/** True when `OPENSPEC_TELEMETRY` is set to anything but an explicit on-value. */
|
|
19
|
+
export function isTelemetryDisabledByEnv(env = process.env) {
|
|
20
|
+
const value = env.OPENSPEC_TELEMETRY;
|
|
21
|
+
return value !== undefined && !ON_VALUES.has(value.trim().toLowerCase());
|
|
22
|
+
}
|
|
23
|
+
/** True when either opt-out signal asks us to stay off the network. */
|
|
24
|
+
export function isTelemetryOptedOutByEnv(env = process.env) {
|
|
25
|
+
return isTelemetryDisabledByEnv(env) || isDoNotTrackSet(env);
|
|
26
|
+
}
|
|
27
|
+
//# sourceMappingURL=opt-out.js.map
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ASCII art animation patterns for the welcome screen.
|
|
3
|
+
* FreeSpec logo animation - diamond/rhombus shape with hollow center "O".
|
|
4
|
+
*/
|
|
5
|
+
/**
|
|
6
|
+
* Welcome animation frames - FreeSpec logo building from center
|
|
7
|
+
* 7 rows × 6 columns diamond with hollow center "O"
|
|
8
|
+
* Center bar is 2 cols × 3 rows (rows 3,4,5 cols 3,4)
|
|
9
|
+
* Each frame is an array of strings (lines of ASCII art)
|
|
10
|
+
* Grid: 6 cols × 2 chars = 12 chars wide
|
|
11
|
+
*/
|
|
12
|
+
export declare const WELCOME_ANIMATION: {
|
|
13
|
+
interval: number;
|
|
14
|
+
frames: string[][];
|
|
15
|
+
};
|
|
16
|
+
//# sourceMappingURL=ascii-patterns.d.ts.map
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ASCII art animation patterns for the welcome screen.
|
|
3
|
+
* FreeSpec logo animation - diamond/rhombus shape with hollow center "O".
|
|
4
|
+
*/
|
|
5
|
+
// Detect if full Unicode is supported
|
|
6
|
+
const supportsUnicode = process.platform !== 'win32' ||
|
|
7
|
+
!!process.env.WT_SESSION || // Windows Terminal
|
|
8
|
+
!!process.env.TERM_PROGRAM; // Modern terminal
|
|
9
|
+
// Character set based on Unicode support
|
|
10
|
+
// Block characters for pixel-art aesthetic
|
|
11
|
+
const CHARS = supportsUnicode
|
|
12
|
+
? { full: '██', dim: '░░', empty: ' ' }
|
|
13
|
+
: { full: '##', dim: '++', empty: ' ' };
|
|
14
|
+
const _ = CHARS.empty;
|
|
15
|
+
const F = CHARS.full;
|
|
16
|
+
const D = CHARS.dim;
|
|
17
|
+
/**
|
|
18
|
+
* Welcome animation frames - FreeSpec logo building from center
|
|
19
|
+
* 7 rows × 6 columns diamond with hollow center "O"
|
|
20
|
+
* Center bar is 2 cols × 3 rows (rows 3,4,5 cols 3,4)
|
|
21
|
+
* Each frame is an array of strings (lines of ASCII art)
|
|
22
|
+
* Grid: 6 cols × 2 chars = 12 chars wide
|
|
23
|
+
*/
|
|
24
|
+
export const WELCOME_ANIMATION = {
|
|
25
|
+
interval: 120,
|
|
26
|
+
frames: [
|
|
27
|
+
// Frame 1: Empty
|
|
28
|
+
[
|
|
29
|
+
`${_}${_}${_}${_}${_}${_}${_}${_}`, // padding row 1
|
|
30
|
+
`${_}${_}${_}${_}${_}${_}${_}${_}`, // padding row 2
|
|
31
|
+
`${_}${_}${_}${_}${_}${_}${_}${_}`, // padding row 3
|
|
32
|
+
`${_}${_}${_}${_}${_}${_}${_}${_}`,
|
|
33
|
+
`${_}${_}${_}${_}${_}${_}${_}${_}`,
|
|
34
|
+
`${_}${_}${_}${_}${_}${_}${_}${_}`,
|
|
35
|
+
`${_}${_}${_}${_}${_}${_}${_}${_}`,
|
|
36
|
+
`${_}${_}${_}${_}${_}${_}${_}${_}`,
|
|
37
|
+
`${_}${_}${_}${_}${_}${_}${_}${_}`,
|
|
38
|
+
`${_}${_}${_}${_}${_}${_}${_}${_}`,
|
|
39
|
+
],
|
|
40
|
+
// Frame 2: Center blocks appear (dim) - 2x3 center bar
|
|
41
|
+
[
|
|
42
|
+
`${_}${_}${_}${_}${_}${_}${_}${_}`, // padding row 1
|
|
43
|
+
`${_}${_}${_}${_}${_}${_}${_}${_}`, // padding row 2
|
|
44
|
+
`${_}${_}${_}${_}${_}${_}${_}${_}`, // padding row 3
|
|
45
|
+
`${_}${_}${_}${_}${_}${_}${_}${_}`,
|
|
46
|
+
`${_}${_}${_}${_}${_}${_}${_}${_}`,
|
|
47
|
+
`${_}${_}${_}${_}${D}${D}${_}${_}`,
|
|
48
|
+
`${_}${_}${_}${_}${D}${D}${_}${_}`,
|
|
49
|
+
`${_}${_}${_}${_}${D}${D}${_}${_}`,
|
|
50
|
+
`${_}${_}${_}${_}${_}${_}${_}${_}`,
|
|
51
|
+
`${_}${_}${_}${_}${_}${_}${_}${_}`,
|
|
52
|
+
],
|
|
53
|
+
// Frame 3: Center blocks solidify
|
|
54
|
+
[
|
|
55
|
+
`${_}${_}${_}${_}${_}${_}${_}${_}`, // padding row 1
|
|
56
|
+
`${_}${_}${_}${_}${_}${_}${_}${_}`, // padding row 2
|
|
57
|
+
`${_}${_}${_}${_}${_}${_}${_}${_}`, // padding row 3
|
|
58
|
+
`${_}${_}${_}${_}${_}${_}${_}${_}`,
|
|
59
|
+
`${_}${_}${_}${_}${_}${_}${_}${_}`,
|
|
60
|
+
`${_}${_}${_}${_}${F}${F}${_}${_}`,
|
|
61
|
+
`${_}${_}${_}${_}${F}${F}${_}${_}`,
|
|
62
|
+
`${_}${_}${_}${_}${F}${F}${_}${_}`,
|
|
63
|
+
`${_}${_}${_}${_}${_}${_}${_}${_}`,
|
|
64
|
+
`${_}${_}${_}${_}${_}${_}${_}${_}`,
|
|
65
|
+
],
|
|
66
|
+
// Frame 4: Top and bottom points appear
|
|
67
|
+
[
|
|
68
|
+
`${_}${_}${_}${_}${_}${_}${_}${_}`, // padding row 1
|
|
69
|
+
`${_}${_}${_}${_}${_}${_}${_}${_}`, // padding row 2
|
|
70
|
+
`${_}${_}${_}${_}${_}${_}${_}${_}`, // padding row 3
|
|
71
|
+
`${_}${_}${_}${_}${D}${D}${_}${_}`,
|
|
72
|
+
`${_}${_}${_}${_}${_}${_}${_}${_}`,
|
|
73
|
+
`${_}${_}${_}${_}${F}${F}${_}${_}`,
|
|
74
|
+
`${_}${_}${_}${_}${F}${F}${_}${_}`,
|
|
75
|
+
`${_}${_}${_}${_}${F}${F}${_}${_}`,
|
|
76
|
+
`${_}${_}${_}${_}${_}${_}${_}${_}`,
|
|
77
|
+
`${_}${_}${_}${_}${D}${D}${_}${_}`,
|
|
78
|
+
],
|
|
79
|
+
// Frame 5: Inner ring forming
|
|
80
|
+
[
|
|
81
|
+
`${_}${_}${_}${_}${_}${_}${_}${_}`, // padding row 1
|
|
82
|
+
`${_}${_}${_}${_}${_}${_}${_}${_}`, // padding row 2
|
|
83
|
+
`${_}${_}${_}${_}${_}${_}${_}${_}`, // padding row 3
|
|
84
|
+
`${_}${_}${_}${_}${F}${F}${_}${_}`,
|
|
85
|
+
`${_}${_}${_}${D}${_}${_}${D}${_}`,
|
|
86
|
+
`${_}${_}${_}${_}${F}${F}${_}${_}`,
|
|
87
|
+
`${_}${_}${_}${_}${F}${F}${_}${_}`,
|
|
88
|
+
`${_}${_}${_}${_}${F}${F}${_}${_}`,
|
|
89
|
+
`${_}${_}${_}${D}${_}${_}${D}${_}`,
|
|
90
|
+
`${_}${_}${_}${_}${F}${F}${_}${_}`,
|
|
91
|
+
],
|
|
92
|
+
// Frame 6: Outer ring appearing
|
|
93
|
+
[
|
|
94
|
+
`${_}${_}${_}${_}${_}${_}${_}${_}`, // padding row 1
|
|
95
|
+
`${_}${_}${_}${_}${_}${_}${_}${_}`, // padding row 2
|
|
96
|
+
`${_}${_}${_}${_}${_}${_}${_}${_}`, // padding row 3
|
|
97
|
+
`${_}${_}${_}${_}${F}${F}${_}${_}`,
|
|
98
|
+
`${_}${_}${_}${F}${_}${_}${F}${_}`,
|
|
99
|
+
`${_}${_}${D}${_}${F}${F}${_}${D}`,
|
|
100
|
+
`${_}${_}${D}${_}${F}${F}${_}${D}`,
|
|
101
|
+
`${_}${_}${D}${_}${F}${F}${_}${D}`,
|
|
102
|
+
`${_}${_}${_}${F}${_}${_}${F}${_}`,
|
|
103
|
+
`${_}${_}${_}${_}${F}${F}${_}${_}`,
|
|
104
|
+
],
|
|
105
|
+
// Frame 7: Full logo
|
|
106
|
+
[
|
|
107
|
+
`${_}${_}${_}${_}${_}${_}${_}${_}`, // padding row 1
|
|
108
|
+
`${_}${_}${_}${_}${_}${_}${_}${_}`, // padding row 2
|
|
109
|
+
`${_}${_}${_}${_}${_}${_}${_}${_}`, // padding row 3
|
|
110
|
+
`${_}${_}${_}${_}${F}${F}${_}${_}`,
|
|
111
|
+
`${_}${_}${_}${F}${_}${_}${F}${_}`,
|
|
112
|
+
`${_}${_}${F}${_}${F}${F}${_}${F}`,
|
|
113
|
+
`${_}${_}${F}${_}${F}${F}${_}${F}`,
|
|
114
|
+
`${_}${_}${F}${_}${F}${F}${_}${F}`,
|
|
115
|
+
`${_}${_}${_}${F}${_}${_}${F}${_}`,
|
|
116
|
+
`${_}${_}${_}${_}${F}${F}${_}${_}`,
|
|
117
|
+
],
|
|
118
|
+
// Frame 8: Hold complete logo
|
|
119
|
+
[
|
|
120
|
+
`${_}${_}${_}${_}${_}${_}${_}${_}`, // padding row 1
|
|
121
|
+
`${_}${_}${_}${_}${_}${_}${_}${_}`, // padding row 2
|
|
122
|
+
`${_}${_}${_}${_}${_}${_}${_}${_}`, // padding row 3
|
|
123
|
+
`${_}${_}${_}${_}${F}${F}${_}${_}`,
|
|
124
|
+
`${_}${_}${_}${F}${_}${_}${F}${_}`,
|
|
125
|
+
`${_}${_}${F}${_}${F}${F}${_}${F}`,
|
|
126
|
+
`${_}${_}${F}${_}${F}${F}${_}${F}`,
|
|
127
|
+
`${_}${_}${F}${_}${F}${F}${_}${F}`,
|
|
128
|
+
`${_}${_}${_}${F}${_}${_}${F}${_}`,
|
|
129
|
+
`${_}${_}${_}${_}${F}${F}${_}${_}`,
|
|
130
|
+
],
|
|
131
|
+
],
|
|
132
|
+
};
|
|
133
|
+
//# sourceMappingURL=ascii-patterns.js.map
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Animated welcome screen for the experimental artifact workflow setup.
|
|
3
|
+
* Shows side-by-side layout with animated ASCII art on left and welcome text on right.
|
|
4
|
+
*/
|
|
5
|
+
/**
|
|
6
|
+
* Best-effort check of the OS-level reduced-motion preference (#722).
|
|
7
|
+
* Any lookup failure (missing binary, unset key, timeout) means
|
|
8
|
+
* "no preference detected" and animation stays enabled.
|
|
9
|
+
*/
|
|
10
|
+
export declare function prefersReducedMotion(platform?: NodeJS.Platform): boolean;
|
|
11
|
+
/**
|
|
12
|
+
* Shows the animated welcome screen.
|
|
13
|
+
* Returns when user presses Enter.
|
|
14
|
+
*/
|
|
15
|
+
export declare function showWelcomeScreen(workflows: readonly string[], options?: {
|
|
16
|
+
animate?: boolean;
|
|
17
|
+
}): Promise<void>;
|
|
18
|
+
//# sourceMappingURL=welcome-screen.d.ts.map
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Animated welcome screen for the experimental artifact workflow setup.
|
|
3
|
+
* Shows side-by-side layout with animated ASCII art on left and welcome text on right.
|
|
4
|
+
*/
|
|
5
|
+
import chalk from 'chalk';
|
|
6
|
+
import { execFileSync, } from 'node:child_process';
|
|
7
|
+
import { WELCOME_ANIMATION } from './ascii-patterns.js';
|
|
8
|
+
import { getOnboardingCommands } from '../core/onboarding-commands.js';
|
|
9
|
+
// Minimum terminal width for side-by-side layout
|
|
10
|
+
const MIN_WIDTH = 60;
|
|
11
|
+
// Width of the ASCII art column (with padding)
|
|
12
|
+
const ART_COLUMN_WIDTH = 24;
|
|
13
|
+
/**
|
|
14
|
+
* Welcome text content (right column)
|
|
15
|
+
*/
|
|
16
|
+
function getWelcomeText(workflows) {
|
|
17
|
+
const onboardingCommands = getOnboardingCommands(workflows);
|
|
18
|
+
const quickStart = [];
|
|
19
|
+
if (onboardingCommands.length > 0) {
|
|
20
|
+
const commandWidth = Math.max(...onboardingCommands.map((c) => c.command.length));
|
|
21
|
+
quickStart.push(chalk.white('Quick start after setup:'));
|
|
22
|
+
for (const { command, description } of onboardingCommands) {
|
|
23
|
+
quickStart.push(` ${chalk.yellow(command.padEnd(commandWidth + 1))} ${chalk.dim(description)}`);
|
|
24
|
+
}
|
|
25
|
+
// These are the canonical names. How each tool spells them differs
|
|
26
|
+
// (/freespec-新建, @freespec-新建, $freespec-新建 ...) and cannot be known
|
|
27
|
+
// until tools are picked, one prompt later — so flag it rather than let the
|
|
28
|
+
// canonical form read as the literal thing to type. "Getting started"
|
|
29
|
+
// prints the real spelling once the selection is known.
|
|
30
|
+
quickStart.push(chalk.dim(' (spelling varies by tool)'));
|
|
31
|
+
quickStart.push('');
|
|
32
|
+
}
|
|
33
|
+
return [
|
|
34
|
+
chalk.white.bold('Welcome to FreeSpec'),
|
|
35
|
+
chalk.dim('变更目录、可选文档、按技能实现'),
|
|
36
|
+
'',
|
|
37
|
+
chalk.white('This setup will configure:'),
|
|
38
|
+
chalk.dim(' • Agent Skills for AI tools'),
|
|
39
|
+
// Not "freespec slash commands": this screen runs before tool selection, and
|
|
40
|
+
// skills-only tools (Codex, Kimi Code, ...) correctly get no command files
|
|
41
|
+
// at all. The exact spelling per tool is printed in "Getting started".
|
|
42
|
+
chalk.dim(' • Workflow commands, if supported'),
|
|
43
|
+
'',
|
|
44
|
+
...quickStart,
|
|
45
|
+
chalk.cyan('Press Enter to select tools...'),
|
|
46
|
+
];
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Renders a single frame with side-by-side layout
|
|
50
|
+
*/
|
|
51
|
+
function renderFrame(artLines, textLines) {
|
|
52
|
+
const maxLines = Math.max(artLines.length, textLines.length);
|
|
53
|
+
const lines = [];
|
|
54
|
+
for (let i = 0; i < maxLines; i++) {
|
|
55
|
+
const artLine = artLines[i] || '';
|
|
56
|
+
const textLine = textLines[i] || '';
|
|
57
|
+
// Pad the art column to fixed width
|
|
58
|
+
const paddedArt = artLine.padEnd(ART_COLUMN_WIDTH);
|
|
59
|
+
// Color the ASCII art with cyan for visual appeal
|
|
60
|
+
const coloredArt = chalk.cyan(paddedArt);
|
|
61
|
+
// Clear line before writing to prevent residual characters
|
|
62
|
+
lines.push(`\x1b[2K${coloredArt}${textLine}`);
|
|
63
|
+
}
|
|
64
|
+
return lines.join('\n');
|
|
65
|
+
}
|
|
66
|
+
const REDUCED_MOTION_EXEC_OPTIONS = {
|
|
67
|
+
encoding: 'utf8',
|
|
68
|
+
timeout: 500,
|
|
69
|
+
// SIGKILL so a wedged lookup can never outlive the timeout and stall init.
|
|
70
|
+
killSignal: 'SIGKILL',
|
|
71
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
72
|
+
};
|
|
73
|
+
/**
|
|
74
|
+
* Best-effort check of the OS-level reduced-motion preference (#722).
|
|
75
|
+
* Any lookup failure (missing binary, unset key, timeout) means
|
|
76
|
+
* "no preference detected" and animation stays enabled.
|
|
77
|
+
*/
|
|
78
|
+
export function prefersReducedMotion(platform = process.platform) {
|
|
79
|
+
try {
|
|
80
|
+
if (platform === 'darwin') {
|
|
81
|
+
// The key only exists once the user has toggled Reduce Motion; when it
|
|
82
|
+
// is unset `defaults` exits non-zero and lands in the catch below.
|
|
83
|
+
const out = execFileSync('defaults', ['read', 'com.apple.universalaccess', 'reduceMotion'], REDUCED_MOTION_EXEC_OPTIONS);
|
|
84
|
+
return out.trim() === '1';
|
|
85
|
+
}
|
|
86
|
+
if (platform === 'linux') {
|
|
87
|
+
const out = execFileSync('gsettings', ['get', 'org.gnome.desktop.interface', 'enable-animations'], REDUCED_MOTION_EXEC_OPTIONS);
|
|
88
|
+
return out.trim() === 'false';
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
catch {
|
|
92
|
+
// Detection is best-effort only.
|
|
93
|
+
}
|
|
94
|
+
return false;
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Checks if the terminal supports animation
|
|
98
|
+
*/
|
|
99
|
+
function canAnimate() {
|
|
100
|
+
// Must be TTY
|
|
101
|
+
if (!process.stdout.isTTY)
|
|
102
|
+
return false;
|
|
103
|
+
// Respect NO_COLOR
|
|
104
|
+
if (process.env.NO_COLOR)
|
|
105
|
+
return false;
|
|
106
|
+
// Manual override for users who need reduced motion (#722). Presence is
|
|
107
|
+
// what counts: even an empty value disables the animation.
|
|
108
|
+
if (process.env.OPENSPEC_NO_ANIMATION !== undefined)
|
|
109
|
+
return false;
|
|
110
|
+
// Check terminal width
|
|
111
|
+
const columns = process.stdout.columns || 80;
|
|
112
|
+
if (columns < MIN_WIDTH)
|
|
113
|
+
return false;
|
|
114
|
+
// Last so only interactive terminals pay for the OS lookup
|
|
115
|
+
if (prefersReducedMotion())
|
|
116
|
+
return false;
|
|
117
|
+
return true;
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Wait for Enter key press
|
|
121
|
+
*/
|
|
122
|
+
async function waitForEnter() {
|
|
123
|
+
if (!process.stdin.isTTY) {
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
// Keep all interactive input on Inquirer's keypress lifecycle. Mixing a raw
|
|
127
|
+
// `data` listener between Inquirer prompts breaks arrow/space keys on Windows.
|
|
128
|
+
const { createPrompt, isEnterKey, useKeypress } = await import('@inquirer/core');
|
|
129
|
+
const prompt = createPrompt((_config, done) => {
|
|
130
|
+
useKeypress((key) => {
|
|
131
|
+
if (key.ctrl && key.name === 'c') {
|
|
132
|
+
process.stdout.write('\n');
|
|
133
|
+
process.exit(0);
|
|
134
|
+
}
|
|
135
|
+
if (isEnterKey(key)) {
|
|
136
|
+
done(undefined);
|
|
137
|
+
}
|
|
138
|
+
});
|
|
139
|
+
return '';
|
|
140
|
+
});
|
|
141
|
+
await prompt({});
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Shows the animated welcome screen.
|
|
145
|
+
* Returns when user presses Enter.
|
|
146
|
+
*/
|
|
147
|
+
export async function showWelcomeScreen(workflows, options = {}) {
|
|
148
|
+
const textLines = getWelcomeText(workflows);
|
|
149
|
+
if (options.animate === false || !canAnimate()) {
|
|
150
|
+
// Fallback: show static welcome. The "Press Enter" line is only honest
|
|
151
|
+
// when we actually wait; in a TTY, returning immediately would let the
|
|
152
|
+
// Enter it asks for fall through into the tool picker and submit the
|
|
153
|
+
// pre-selected tools sight-unseen. Without a TTY, drop the line instead.
|
|
154
|
+
const staticLines = process.stdin.isTTY
|
|
155
|
+
? textLines
|
|
156
|
+
: textLines.filter((line) => !line.includes('Press Enter'));
|
|
157
|
+
const frame = WELCOME_ANIMATION.frames[3]; // Peak frame
|
|
158
|
+
process.stdout.write('\n' + renderFrame(frame, staticLines) + '\n\n');
|
|
159
|
+
await waitForEnter();
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
let frameIndex = 0;
|
|
163
|
+
let running = true;
|
|
164
|
+
let isFirstRender = true;
|
|
165
|
+
// Content height for cursor movement between frames
|
|
166
|
+
const numContentLines = Math.max(WELCOME_ANIMATION.frames[0].length, textLines.length);
|
|
167
|
+
const frameHeight = numContentLines + 1; // internal newlines (11) + trailing newlines (2) = 13
|
|
168
|
+
// Total height including initial newline (for cleanup)
|
|
169
|
+
const totalHeight = frameHeight + 1; // 14
|
|
170
|
+
// Initial render
|
|
171
|
+
process.stdout.write('\n');
|
|
172
|
+
// Animation loop
|
|
173
|
+
const interval = setInterval(() => {
|
|
174
|
+
if (!running)
|
|
175
|
+
return;
|
|
176
|
+
const frame = WELCOME_ANIMATION.frames[frameIndex];
|
|
177
|
+
// Move cursor up to overwrite previous frame (always after first render)
|
|
178
|
+
if (!isFirstRender) {
|
|
179
|
+
process.stdout.write(`\x1b[${frameHeight}A`);
|
|
180
|
+
}
|
|
181
|
+
isFirstRender = false;
|
|
182
|
+
// Render current frame
|
|
183
|
+
process.stdout.write(renderFrame(frame, textLines) + '\n\n');
|
|
184
|
+
// Advance to next frame
|
|
185
|
+
frameIndex = (frameIndex + 1) % WELCOME_ANIMATION.frames.length;
|
|
186
|
+
}, WELCOME_ANIMATION.interval);
|
|
187
|
+
// Wait for Enter
|
|
188
|
+
await waitForEnter();
|
|
189
|
+
// Stop animation
|
|
190
|
+
running = false;
|
|
191
|
+
clearInterval(interval);
|
|
192
|
+
// Clear the welcome screen and move on
|
|
193
|
+
process.stdout.write(`\x1b[${totalHeight}A`);
|
|
194
|
+
for (let i = 0; i < totalHeight; i++) {
|
|
195
|
+
process.stdout.write('\x1b[2K\n'); // Clear line
|
|
196
|
+
}
|
|
197
|
+
process.stdout.write(`\x1b[${totalHeight}A`); // Move back up
|
|
198
|
+
}
|
|
199
|
+
//# sourceMappingURL=welcome-screen.js.map
|