@fission-ai/openspec 1.6.0 → 1.8.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/README.md +31 -2
- package/dist/cli/index.js +79 -8
- package/dist/commands/change.d.ts +0 -1
- package/dist/commands/change.js +105 -51
- package/dist/commands/config.js +14 -8
- package/dist/commands/doctor.js +9 -4
- package/dist/commands/feedback.js +74 -23
- package/dist/commands/schema.js +110 -31
- package/dist/commands/show.js +5 -2
- package/dist/commands/spec.js +49 -29
- package/dist/commands/validate.js +20 -5
- package/dist/commands/workflow/index.d.ts +1 -1
- package/dist/commands/workflow/index.js +1 -1
- package/dist/commands/workflow/instructions.d.ts +7 -1
- package/dist/commands/workflow/instructions.js +121 -33
- package/dist/commands/workflow/shared.d.ts +13 -2
- package/dist/commands/workflow/shared.js +34 -4
- package/dist/commands/workflow/status.js +9 -4
- package/dist/commands/workflow/templates.js +15 -5
- package/dist/core/archive.d.ts +14 -1
- package/dist/core/archive.js +1332 -155
- package/dist/core/artifact-graph/graph.d.ts +16 -0
- package/dist/core/artifact-graph/graph.js +30 -6
- package/dist/core/artifact-graph/index.d.ts +1 -1
- package/dist/core/artifact-graph/index.js +1 -1
- package/dist/core/artifact-graph/instruction-loader.d.ts +30 -3
- package/dist/core/artifact-graph/instruction-loader.js +70 -12
- package/dist/core/artifact-graph/outputs.d.ts +1 -0
- package/dist/core/artifact-graph/outputs.js +67 -5
- package/dist/core/artifact-graph/resolver.d.ts +14 -0
- package/dist/core/artifact-graph/resolver.js +69 -15
- package/dist/core/artifact-graph/types.js +21 -3
- package/dist/core/available-tools.d.ts +2 -2
- package/dist/core/available-tools.js +14 -3
- package/dist/core/change-metadata/schema.d.ts +2 -0
- package/dist/core/change-metadata/schema.js +13 -0
- package/dist/core/change-status-policy.d.ts +1 -1
- package/dist/core/change-status-policy.js +2 -2
- package/dist/core/command-generation/adapters/amazon-q.d.ts +4 -0
- package/dist/core/command-generation/adapters/amazon-q.js +7 -1
- package/dist/core/command-generation/adapters/antigravity.js +2 -1
- package/dist/core/command-generation/adapters/auggie.js +2 -1
- package/dist/core/command-generation/adapters/bob.d.ts +5 -1
- package/dist/core/command-generation/adapters/bob.js +6 -5
- package/dist/core/command-generation/adapters/claude.js +1 -8
- package/dist/core/command-generation/adapters/codebuddy.js +3 -2
- package/dist/core/command-generation/adapters/continue.js +3 -2
- package/dist/core/command-generation/adapters/costrict.js +2 -1
- package/dist/core/command-generation/adapters/crush.js +5 -5
- package/dist/core/command-generation/adapters/cursor.js +2 -2
- 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/factory.js +2 -1
- package/dist/core/command-generation/adapters/gemini.js +37 -2
- package/dist/core/command-generation/adapters/github-copilot.js +2 -1
- package/dist/core/command-generation/adapters/iflow.js +5 -4
- package/dist/core/command-generation/adapters/index.d.ts +2 -2
- package/dist/core/command-generation/adapters/index.js +2 -2
- package/dist/core/command-generation/adapters/junie.js +2 -1
- package/dist/core/command-generation/adapters/kiro.js +2 -1
- package/dist/core/command-generation/adapters/lingma.js +5 -5
- package/dist/core/command-generation/adapters/oh-my-pi.d.ts +2 -2
- package/dist/core/command-generation/adapters/oh-my-pi.js +3 -5
- package/dist/core/command-generation/adapters/opencode.js +3 -5
- package/dist/core/command-generation/adapters/pi.d.ts +2 -2
- package/dist/core/command-generation/adapters/pi.js +3 -6
- package/dist/core/command-generation/adapters/qoder.js +5 -5
- package/dist/core/command-generation/adapters/qwen.d.ts +7 -3
- package/dist/core/command-generation/adapters/qwen.js +12 -7
- package/dist/core/command-generation/adapters/roocode.d.ts +4 -4
- package/dist/core/command-generation/adapters/roocode.js +4 -4
- package/dist/core/command-generation/adapters/trae.js +1 -21
- 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 +8 -0
- package/dist/core/command-generation/generator.js +15 -1
- package/dist/core/command-generation/index.d.ts +1 -1
- package/dist/core/command-generation/index.js +1 -1
- 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.js +4 -4
- package/dist/core/command-generation/types.d.ts +9 -2
- package/dist/core/command-generation/yaml.d.ts +20 -6
- package/dist/core/command-generation/yaml.js +31 -21
- package/dist/core/command-surface.d.ts +15 -0
- package/dist/core/command-surface.js +33 -0
- package/dist/core/completions/command-registry.js +16 -2
- package/dist/core/completions/generators/powershell-generator.js +15 -8
- package/dist/core/completions/installers/zsh-installer.d.ts +9 -0
- package/dist/core/completions/installers/zsh-installer.js +20 -4
- package/dist/core/config-prompts.d.ts +1 -1
- package/dist/core/config-prompts.js +14 -1
- package/dist/core/config-schema.d.ts +10 -1
- package/dist/core/config-schema.js +67 -1
- package/dist/core/config.d.ts +15 -0
- package/dist/core/config.js +45 -5
- package/dist/core/file-state.js +56 -24
- package/dist/core/github-copilot/cloud-agent.d.ts +95 -0
- package/dist/core/github-copilot/cloud-agent.js +497 -0
- package/dist/core/global-config.d.ts +16 -0
- package/dist/core/init.d.ts +58 -0
- package/dist/core/init.js +450 -69
- package/dist/core/legacy-cleanup.d.ts +59 -0
- package/dist/core/legacy-cleanup.js +253 -24
- package/dist/core/list.js +5 -7
- package/dist/core/migration.d.ts +73 -1
- package/dist/core/migration.js +360 -11
- package/dist/core/onboarding-commands.d.ts +30 -0
- package/dist/core/onboarding-commands.js +38 -0
- package/dist/core/parsers/change-parser.d.ts +17 -2
- package/dist/core/parsers/change-parser.js +33 -20
- package/dist/core/parsers/code-fence.d.ts +15 -0
- package/dist/core/parsers/code-fence.js +49 -0
- package/dist/core/parsers/markdown-parser.js +2 -1
- package/dist/core/parsers/requirement-blocks.d.ts +18 -0
- package/dist/core/parsers/requirement-blocks.js +120 -35
- package/dist/core/parsers/requirement-text.d.ts +1 -8
- package/dist/core/parsers/requirement-text.js +4 -42
- package/dist/core/parsers/spec-structure.d.ts +1 -1
- package/dist/core/parsers/spec-structure.js +20 -30
- package/dist/core/profile-sync-drift.d.ts +0 -8
- package/dist/core/profile-sync-drift.js +48 -49
- package/dist/core/project-config.d.ts +28 -6
- package/dist/core/project-config.js +108 -8
- package/dist/core/references.js +42 -4
- package/dist/core/relationship-health.d.ts +8 -0
- package/dist/core/relationship-health.js +13 -0
- package/dist/core/root-selection.d.ts +9 -6
- package/dist/core/root-selection.js +42 -5
- package/dist/core/shared/index.d.ts +1 -0
- package/dist/core/shared/index.js +1 -0
- package/dist/core/shared/skill-content-equivalence.d.ts +7 -0
- package/dist/core/shared/skill-content-equivalence.js +41 -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 +25 -5
- package/dist/core/shared/tool-detection.js +166 -36
- package/dist/core/shared-skill-target.d.ts +16 -0
- package/dist/core/shared-skill-target.js +156 -0
- package/dist/core/specs-apply.d.ts +80 -33
- package/dist/core/specs-apply.js +659 -171
- package/dist/core/store/git.d.ts +13 -0
- package/dist/core/store/git.js +24 -0
- package/dist/core/templates/skill-templates.d.ts +1 -1
- package/dist/core/templates/skill-templates.js +1 -1
- package/dist/core/templates/workflows/apply-change.d.ts +10 -0
- package/dist/core/templates/workflows/apply-change.js +48 -158
- package/dist/core/templates/workflows/archive-change.js +170 -42
- package/dist/core/templates/workflows/bulk-archive-change.js +256 -75
- package/dist/core/templates/workflows/continue-change.js +38 -46
- package/dist/core/templates/workflows/explore.js +54 -18
- package/dist/core/templates/workflows/ff-change.js +46 -30
- package/dist/core/templates/workflows/new-change.js +2 -2
- package/dist/core/templates/workflows/onboard.js +13 -7
- package/dist/core/templates/workflows/propose.js +116 -46
- package/dist/core/templates/workflows/store-selection.d.ts +1 -1
- package/dist/core/templates/workflows/store-selection.js +1 -1
- package/dist/core/templates/workflows/sync-specs.js +260 -32
- package/dist/core/templates/workflows/update-change.js +26 -16
- package/dist/core/templates/workflows/verify-change.js +16 -10
- package/dist/core/update.d.ts +40 -5
- package/dist/core/update.js +389 -93
- package/dist/core/validation/constants.d.ts +4 -1
- package/dist/core/validation/constants.js +4 -1
- package/dist/core/validation/validator.d.ts +22 -8
- package/dist/core/validation/validator.js +222 -50
- package/dist/core/version-check.d.ts +152 -0
- package/dist/core/version-check.js +731 -0
- package/dist/core/view.js +12 -17
- package/dist/prompts/searchable-multi-select.js +1 -1
- package/dist/telemetry/config.d.ts +3 -4
- package/dist/telemetry/index.d.ts +10 -5
- package/dist/telemetry/index.js +75 -41
- package/dist/ui/welcome-screen.d.ts +9 -1
- package/dist/ui/welcome-screen.js +91 -38
- package/dist/utils/change-metadata.d.ts +43 -0
- package/dist/utils/change-metadata.js +123 -7
- package/dist/utils/change-utils.d.ts +10 -4
- package/dist/utils/change-utils.js +20 -13
- package/dist/utils/ci.d.ts +12 -0
- package/dist/utils/ci.js +16 -0
- package/dist/utils/command-references.d.ts +92 -6
- package/dist/utils/command-references.js +180 -7
- package/dist/utils/date.d.ts +8 -0
- package/dist/utils/date.js +13 -0
- package/dist/utils/file-system.d.ts +10 -0
- package/dist/utils/file-system.js +70 -0
- package/dist/utils/index.d.ts +1 -1
- package/dist/utils/index.js +1 -1
- package/dist/utils/interactive.d.ts +23 -0
- package/dist/utils/interactive.js +32 -0
- package/dist/utils/item-discovery.d.ts +19 -0
- package/dist/utils/item-discovery.js +30 -48
- package/dist/utils/shell-detection.d.ts +1 -1
- package/dist/utils/shell-detection.js +64 -11
- package/dist/utils/spec-discovery.d.ts +40 -0
- package/dist/utils/spec-discovery.js +115 -0
- package/dist/utils/task-progress.d.ts +17 -0
- package/dist/utils/task-progress.js +41 -13
- package/package.json +14 -12
- package/schemas/spec-driven/schema.yaml +67 -10
- package/schemas/spec-driven/templates/design.md +2 -2
- package/schemas/spec-driven/templates/proposal.md +10 -4
- package/schemas/spec-driven/templates/spec.md +3 -0
- package/dist/core/command-generation/adapters/codex.d.ts +0 -16
- package/dist/core/command-generation/adapters/codex.js +0 -39
- package/dist/core/command-generation/adapters/windsurf.d.ts +0 -14
- package/dist/core/command-generation/adapters/windsurf.js +0 -38
package/dist/core/view.js
CHANGED
|
@@ -3,6 +3,7 @@ import * as path from 'path';
|
|
|
3
3
|
import chalk from 'chalk';
|
|
4
4
|
import { getTaskProgressForChange } from '../utils/task-progress.js';
|
|
5
5
|
import { MarkdownParser } from './parsers/markdown-parser.js';
|
|
6
|
+
import { discoverSpecFiles } from '../utils/spec-discovery.js';
|
|
6
7
|
export class ViewCommand {
|
|
7
8
|
async execute(targetPath = '.') {
|
|
8
9
|
const openspecDir = path.join(targetPath, 'openspec');
|
|
@@ -106,23 +107,17 @@ export class ViewCommand {
|
|
|
106
107
|
return [];
|
|
107
108
|
}
|
|
108
109
|
const specs = [];
|
|
109
|
-
const
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
const
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
}
|
|
121
|
-
catch (error) {
|
|
122
|
-
// If spec cannot be parsed, include with 0 count
|
|
123
|
-
specs.push({ name: entry.name, requirementCount: 0 });
|
|
124
|
-
}
|
|
125
|
-
}
|
|
110
|
+
for (const { id, specFile } of await discoverSpecFiles(specsDir)) {
|
|
111
|
+
try {
|
|
112
|
+
const content = fs.readFileSync(specFile, 'utf-8');
|
|
113
|
+
const parser = new MarkdownParser(content);
|
|
114
|
+
const spec = parser.parseSpec(id);
|
|
115
|
+
const requirementCount = spec.requirements.length;
|
|
116
|
+
specs.push({ name: id, requirementCount });
|
|
117
|
+
}
|
|
118
|
+
catch (error) {
|
|
119
|
+
// If spec cannot be parsed, include with 0 count
|
|
120
|
+
specs.push({ name: id, requirementCount: 0 });
|
|
126
121
|
}
|
|
127
122
|
}
|
|
128
123
|
return specs;
|
|
@@ -113,7 +113,7 @@ async function createSearchableMultiSelect() {
|
|
|
113
113
|
const actualIndex = startIndex + i;
|
|
114
114
|
const isActive = actualIndex === cursor;
|
|
115
115
|
const selected = selectedSet.has(item.value);
|
|
116
|
-
const icon = selected ? chalk.green('
|
|
116
|
+
const icon = selected ? chalk.green('[x]') : chalk.dim('[ ]');
|
|
117
117
|
const arrow = isActive ? chalk.cyan('›') : ' ';
|
|
118
118
|
const name = isActive ? chalk.cyan(item.name) : item.name;
|
|
119
119
|
const isRefresh = selected && item.configured;
|
|
@@ -1,9 +1,8 @@
|
|
|
1
|
+
import { type TelemetryConfig } from '../core/global-config.js';
|
|
1
2
|
export declare const CONFIG_DIR_NAME = "openspec";
|
|
2
3
|
export declare const CONFIG_FILE_NAME = "config.json";
|
|
3
|
-
export
|
|
4
|
-
|
|
5
|
-
noticeSeen?: boolean;
|
|
6
|
-
}
|
|
4
|
+
/** Re-export shared telemetry section type (single source of truth in global-config). */
|
|
5
|
+
export type { TelemetryConfig };
|
|
7
6
|
export interface GlobalConfig {
|
|
8
7
|
telemetry?: TelemetryConfig;
|
|
9
8
|
[key: string]: unknown;
|
|
@@ -1,10 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Check if telemetry is enabled.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
4
|
+
* Precedence (first match wins):
|
|
5
|
+
* 1. OPENSPEC_TELEMETRY=0 → disabled
|
|
6
|
+
* 2. DO_NOT_TRACK=1 → disabled
|
|
7
|
+
* 3. CI set to a truthy/on value → disabled (same rule as version-check)
|
|
8
|
+
* 4. global config telemetry.enabled === false → disabled
|
|
9
|
+
* 5. otherwise enabled (unset config means on; opt-out model)
|
|
10
|
+
*
|
|
11
|
+
* Kept synchronous so call sites need not become async. Reads config via
|
|
12
|
+
* sync getGlobalConfig() rather than async getTelemetryConfig().
|
|
8
13
|
*/
|
|
9
14
|
export declare function isTelemetryEnabled(): boolean;
|
|
10
15
|
/**
|
|
@@ -24,7 +29,7 @@ export declare function trackCommand(commandName: string, version: string): Prom
|
|
|
24
29
|
*/
|
|
25
30
|
export declare function maybeShowTelemetryNotice(): Promise<void>;
|
|
26
31
|
/**
|
|
27
|
-
*
|
|
32
|
+
* Flush pending telemetry events.
|
|
28
33
|
* Call this before CLI exit.
|
|
29
34
|
*/
|
|
30
35
|
export declare function shutdown(): Promise<void>;
|
package/dist/telemetry/index.js
CHANGED
|
@@ -4,12 +4,24 @@
|
|
|
4
4
|
* Privacy-first design:
|
|
5
5
|
* - Only tracks command name and version
|
|
6
6
|
* - No arguments, file paths, or content
|
|
7
|
-
* - Opt-out via OPENSPEC_TELEMETRY=0
|
|
7
|
+
* - Opt-out via OPENSPEC_TELEMETRY=0, DO_NOT_TRACK=1, or
|
|
8
|
+
* `openspec config set telemetry.enabled false`
|
|
8
9
|
* - Auto-disabled in CI environments
|
|
9
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).
|
|
10
21
|
*/
|
|
11
|
-
import { PostHog } from 'posthog-node';
|
|
12
22
|
import { randomUUID } from 'crypto';
|
|
23
|
+
import { getGlobalConfig } from '../core/global-config.js';
|
|
24
|
+
import { isCiEnvironment } from '../utils/ci.js';
|
|
13
25
|
import { getTelemetryConfig, updateTelemetryConfig } from './config.js';
|
|
14
26
|
// PostHog API key - public key for client-side analytics
|
|
15
27
|
// This is safe to embed as it only allows sending events, not reading data
|
|
@@ -17,11 +29,23 @@ const POSTHOG_API_KEY = 'phc_Hthu8YvaIJ9QaFKyTG4TbVwkbd5ktcAFzVTKeMmoW2g';
|
|
|
17
29
|
// Using reverse proxy to avoid ad blockers and keep traffic on our domain
|
|
18
30
|
const POSTHOG_HOST = 'https://edge.openspec.dev';
|
|
19
31
|
const TELEMETRY_REQUEST_TIMEOUT_MS = 1000;
|
|
20
|
-
let posthogClient = null;
|
|
21
32
|
let anonymousId = null;
|
|
33
|
+
/**
|
|
34
|
+
* Requests started by trackCommand and not yet settled, so shutdown can
|
|
35
|
+
* flush them before the process exits. Each request is individually
|
|
36
|
+
* time-bounded, so awaiting them cannot stall exit for more than the
|
|
37
|
+
* request timeout.
|
|
38
|
+
*/
|
|
39
|
+
const pendingEvents = new Set();
|
|
22
40
|
async function safeTelemetryFetch(url, options) {
|
|
23
41
|
try {
|
|
24
42
|
const response = await fetch(url, options);
|
|
43
|
+
// Telemetry never reads the body, but undici keeps the connection
|
|
44
|
+
// occupied until the body is consumed or canceled — dispose of it on
|
|
45
|
+
// every path so no socket outlives shutdown().
|
|
46
|
+
if (response.body) {
|
|
47
|
+
await response.body.cancel();
|
|
48
|
+
}
|
|
25
49
|
if (response.ok) {
|
|
26
50
|
return response;
|
|
27
51
|
}
|
|
@@ -34,10 +58,15 @@ async function safeTelemetryFetch(url, options) {
|
|
|
34
58
|
/**
|
|
35
59
|
* Check if telemetry is enabled.
|
|
36
60
|
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
61
|
+
* Precedence (first match wins):
|
|
62
|
+
* 1. OPENSPEC_TELEMETRY=0 → disabled
|
|
63
|
+
* 2. DO_NOT_TRACK=1 → disabled
|
|
64
|
+
* 3. CI set to a truthy/on value → disabled (same rule as version-check)
|
|
65
|
+
* 4. global config telemetry.enabled === false → disabled
|
|
66
|
+
* 5. otherwise enabled (unset config means on; opt-out model)
|
|
67
|
+
*
|
|
68
|
+
* Kept synchronous so call sites need not become async. Reads config via
|
|
69
|
+
* sync getGlobalConfig() rather than async getTelemetryConfig().
|
|
41
70
|
*/
|
|
42
71
|
export function isTelemetryEnabled() {
|
|
43
72
|
// Check explicit opt-out
|
|
@@ -48,8 +77,12 @@ export function isTelemetryEnabled() {
|
|
|
48
77
|
if (process.env.DO_NOT_TRACK === '1') {
|
|
49
78
|
return false;
|
|
50
79
|
}
|
|
51
|
-
// Auto-disable in CI environments
|
|
52
|
-
if (
|
|
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) {
|
|
53
86
|
return false;
|
|
54
87
|
}
|
|
55
88
|
return true;
|
|
@@ -75,24 +108,30 @@ export async function getOrCreateAnonymousId() {
|
|
|
75
108
|
return anonymousId;
|
|
76
109
|
}
|
|
77
110
|
/**
|
|
78
|
-
*
|
|
79
|
-
*
|
|
111
|
+
* Send one capture event to PostHog's batch endpoint. Fire-and-forget:
|
|
112
|
+
* bounded by the request timeout, never throws, never retries.
|
|
80
113
|
*/
|
|
81
|
-
function
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
}
|
|
95
|
-
|
|
114
|
+
function sendEvent(distinctId, event, properties) {
|
|
115
|
+
const body = JSON.stringify({
|
|
116
|
+
api_key: POSTHOG_API_KEY,
|
|
117
|
+
batch: [
|
|
118
|
+
{
|
|
119
|
+
type: 'capture',
|
|
120
|
+
event,
|
|
121
|
+
distinct_id: distinctId,
|
|
122
|
+
properties,
|
|
123
|
+
timestamp: new Date().toISOString(),
|
|
124
|
+
},
|
|
125
|
+
],
|
|
126
|
+
});
|
|
127
|
+
const request = safeTelemetryFetch(`${POSTHOG_HOST}/batch/`, {
|
|
128
|
+
method: 'POST',
|
|
129
|
+
headers: { 'content-type': 'application/json' },
|
|
130
|
+
body,
|
|
131
|
+
signal: AbortSignal.timeout(TELEMETRY_REQUEST_TIMEOUT_MS),
|
|
132
|
+
}).then(() => undefined);
|
|
133
|
+
pendingEvents.add(request);
|
|
134
|
+
void request.finally(() => pendingEvents.delete(request));
|
|
96
135
|
}
|
|
97
136
|
/**
|
|
98
137
|
* Track a command execution.
|
|
@@ -106,16 +145,11 @@ export async function trackCommand(commandName, version) {
|
|
|
106
145
|
}
|
|
107
146
|
try {
|
|
108
147
|
const userId = await getOrCreateAnonymousId();
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
command: commandName,
|
|
115
|
-
version: version,
|
|
116
|
-
surface: 'cli',
|
|
117
|
-
$ip: null, // Explicitly disable IP tracking
|
|
118
|
-
},
|
|
148
|
+
sendEvent(userId, 'command_executed', {
|
|
149
|
+
command: commandName,
|
|
150
|
+
version: version,
|
|
151
|
+
surface: 'cli',
|
|
152
|
+
$ip: null, // Explicitly disable IP tracking
|
|
119
153
|
});
|
|
120
154
|
}
|
|
121
155
|
catch {
|
|
@@ -135,7 +169,7 @@ export async function maybeShowTelemetryNotice() {
|
|
|
135
169
|
return;
|
|
136
170
|
}
|
|
137
171
|
// Display notice
|
|
138
|
-
console.log('Note: OpenSpec collects anonymous usage stats. Opt out: OPENSPEC_TELEMETRY=0');
|
|
172
|
+
console.log('Note: OpenSpec collects anonymous usage stats. Opt out: OPENSPEC_TELEMETRY=0 or openspec config set telemetry.enabled false');
|
|
139
173
|
// Mark as seen
|
|
140
174
|
await updateTelemetryConfig({ noticeSeen: true });
|
|
141
175
|
}
|
|
@@ -144,21 +178,21 @@ export async function maybeShowTelemetryNotice() {
|
|
|
144
178
|
}
|
|
145
179
|
}
|
|
146
180
|
/**
|
|
147
|
-
*
|
|
181
|
+
* Flush pending telemetry events.
|
|
148
182
|
* Call this before CLI exit.
|
|
149
183
|
*/
|
|
150
184
|
export async function shutdown() {
|
|
151
|
-
if (
|
|
185
|
+
if (pendingEvents.size === 0) {
|
|
152
186
|
return;
|
|
153
187
|
}
|
|
154
188
|
try {
|
|
155
|
-
await
|
|
189
|
+
await Promise.allSettled([...pendingEvents]);
|
|
156
190
|
}
|
|
157
191
|
catch {
|
|
158
192
|
// Silent failure - telemetry should never break CLI exit
|
|
159
193
|
}
|
|
160
194
|
finally {
|
|
161
|
-
|
|
195
|
+
pendingEvents.clear();
|
|
162
196
|
}
|
|
163
197
|
}
|
|
164
198
|
//# sourceMappingURL=index.js.map
|
|
@@ -2,9 +2,17 @@
|
|
|
2
2
|
* Animated welcome screen for the experimental artifact workflow setup.
|
|
3
3
|
* Shows side-by-side layout with animated ASCII art on left and welcome text on right.
|
|
4
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;
|
|
5
11
|
/**
|
|
6
12
|
* Shows the animated welcome screen.
|
|
7
13
|
* Returns when user presses Enter.
|
|
8
14
|
*/
|
|
9
|
-
export declare function showWelcomeScreen(
|
|
15
|
+
export declare function showWelcomeScreen(workflows: readonly string[], options?: {
|
|
16
|
+
animate?: boolean;
|
|
17
|
+
}): Promise<void>;
|
|
10
18
|
//# sourceMappingURL=welcome-screen.d.ts.map
|
|
@@ -3,7 +3,9 @@
|
|
|
3
3
|
* Shows side-by-side layout with animated ASCII art on left and welcome text on right.
|
|
4
4
|
*/
|
|
5
5
|
import chalk from 'chalk';
|
|
6
|
+
import { execFileSync, } from 'node:child_process';
|
|
6
7
|
import { WELCOME_ANIMATION } from './ascii-patterns.js';
|
|
8
|
+
import { getOnboardingCommands } from '../core/onboarding-commands.js';
|
|
7
9
|
// Minimum terminal width for side-by-side layout
|
|
8
10
|
const MIN_WIDTH = 60;
|
|
9
11
|
// Width of the ASCII art column (with padding)
|
|
@@ -11,20 +13,35 @@ const ART_COLUMN_WIDTH = 24;
|
|
|
11
13
|
/**
|
|
12
14
|
* Welcome text content (right column)
|
|
13
15
|
*/
|
|
14
|
-
function getWelcomeText() {
|
|
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
|
+
// (/opsx-propose, @opsx-propose, $openspec-propose ...) 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
|
+
}
|
|
15
33
|
return [
|
|
16
34
|
chalk.white.bold('Welcome to OpenSpec'),
|
|
17
35
|
chalk.dim('A lightweight spec-driven framework'),
|
|
18
36
|
'',
|
|
19
37
|
chalk.white('This setup will configure:'),
|
|
20
38
|
chalk.dim(' • Agent Skills for AI tools'),
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
` ${chalk.yellow('/opsx:continue')} ${chalk.dim('Next artifact')}`,
|
|
26
|
-
` ${chalk.yellow('/opsx:apply')} ${chalk.dim('Implement tasks')}`,
|
|
39
|
+
// Not "opsx 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'),
|
|
27
43
|
'',
|
|
44
|
+
...quickStart,
|
|
28
45
|
chalk.cyan('Press Enter to select tools...'),
|
|
29
46
|
];
|
|
30
47
|
}
|
|
@@ -46,6 +63,36 @@ function renderFrame(artLines, textLines) {
|
|
|
46
63
|
}
|
|
47
64
|
return lines.join('\n');
|
|
48
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
|
+
}
|
|
49
96
|
/**
|
|
50
97
|
* Checks if the terminal supports animation
|
|
51
98
|
*/
|
|
@@ -56,54 +103,60 @@ function canAnimate() {
|
|
|
56
103
|
// Respect NO_COLOR
|
|
57
104
|
if (process.env.NO_COLOR)
|
|
58
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;
|
|
59
110
|
// Check terminal width
|
|
60
111
|
const columns = process.stdout.columns || 80;
|
|
61
112
|
if (columns < MIN_WIDTH)
|
|
62
113
|
return false;
|
|
114
|
+
// Last so only interactive terminals pay for the OS lookup
|
|
115
|
+
if (prefersReducedMotion())
|
|
116
|
+
return false;
|
|
63
117
|
return true;
|
|
64
118
|
}
|
|
65
119
|
/**
|
|
66
120
|
* Wait for Enter key press
|
|
67
121
|
*/
|
|
68
|
-
function waitForEnter() {
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
stdin.removeListener('data', onData);
|
|
84
|
-
stdin.setRawMode(wasRaw);
|
|
85
|
-
stdin.pause();
|
|
86
|
-
// Handle Ctrl+C
|
|
87
|
-
if (char === '\u0003') {
|
|
88
|
-
process.stdout.write('\n');
|
|
89
|
-
process.exit(0);
|
|
90
|
-
}
|
|
91
|
-
resolve();
|
|
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);
|
|
92
137
|
}
|
|
93
|
-
};
|
|
94
|
-
|
|
138
|
+
});
|
|
139
|
+
return '';
|
|
95
140
|
});
|
|
141
|
+
await prompt({});
|
|
96
142
|
}
|
|
97
143
|
/**
|
|
98
144
|
* Shows the animated welcome screen.
|
|
99
145
|
* Returns when user presses Enter.
|
|
100
146
|
*/
|
|
101
|
-
export async function showWelcomeScreen() {
|
|
102
|
-
const textLines = getWelcomeText();
|
|
103
|
-
if (!canAnimate()) {
|
|
104
|
-
// Fallback: show static welcome
|
|
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'));
|
|
105
157
|
const frame = WELCOME_ANIMATION.frames[3]; // Peak frame
|
|
106
|
-
process.stdout.write('\n' + renderFrame(frame,
|
|
158
|
+
process.stdout.write('\n' + renderFrame(frame, staticLines) + '\n\n');
|
|
159
|
+
await waitForEnter();
|
|
107
160
|
return;
|
|
108
161
|
}
|
|
109
162
|
let frameIndex = 0;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type ChangeMetadata } from '../core/change-metadata/index.js';
|
|
2
|
+
import { type ProjectConfig } from '../core/project-config.js';
|
|
2
3
|
export declare const METADATA_FILENAME = ".openspec.yaml";
|
|
3
4
|
/**
|
|
4
5
|
* Error thrown when change metadata validation fails.
|
|
@@ -37,6 +38,8 @@ export declare function writeChangeMetadata(changeDir: string, metadata: ChangeM
|
|
|
37
38
|
export declare function readChangeMetadata(changeDir: string, projectRoot?: string): ChangeMetadata | null;
|
|
38
39
|
export interface ResolveSchemaForChangeOptions {
|
|
39
40
|
metadata?: ChangeMetadata | null;
|
|
41
|
+
/** Pre-read project config; suppresses the fallback config read when provided. */
|
|
42
|
+
projectConfig?: ProjectConfig | null;
|
|
40
43
|
}
|
|
41
44
|
/**
|
|
42
45
|
* Resolves the schema for a change, with explicit override taking precedence.
|
|
@@ -52,4 +55,44 @@ export interface ResolveSchemaForChangeOptions {
|
|
|
52
55
|
* @returns The resolved schema name
|
|
53
56
|
*/
|
|
54
57
|
export declare function resolveSchemaForChange(changeDir: string, explicitSchema?: string, projectRootOverride?: string, options?: ResolveSchemaForChangeOptions): string;
|
|
58
|
+
export interface MetadataMarker {
|
|
59
|
+
/**
|
|
60
|
+
* True when the metadata parses under ChangeMetadataSchema, sets
|
|
61
|
+
* the requested boolean marker to true, and names a schema that loads.
|
|
62
|
+
*/
|
|
63
|
+
declared: boolean;
|
|
64
|
+
/**
|
|
65
|
+
* Set when the marker cannot be honored: it appears in a file that
|
|
66
|
+
* fails the metadata contract, or the metadata file exists but cannot be
|
|
67
|
+
* read at all (so whether the marker is set cannot even be determined).
|
|
68
|
+
*/
|
|
69
|
+
invalidReason?: string;
|
|
70
|
+
}
|
|
71
|
+
/** @deprecated Use MetadataMarker. */
|
|
72
|
+
export type SkipSpecsMarker = MetadataMarker;
|
|
73
|
+
/**
|
|
74
|
+
* Non-throwing read of the skip_specs marker. The marker only counts when the
|
|
75
|
+
* metadata would load for status/instructions: the file parses under
|
|
76
|
+
* ChangeMetadataSchema, its schema name passes readChangeMetadata's
|
|
77
|
+
* listSchemas membership check, AND the schema itself loads via resolveSchema
|
|
78
|
+
* (a schema.yaml that exists but does not parse fails status just the same).
|
|
79
|
+
* Validate and archive must never honor metadata the rest of the CLI rejects,
|
|
80
|
+
* in either direction. The project root for schema resolution is derived from
|
|
81
|
+
* changeDir exactly like resolveSchemaForChange (changeDir is
|
|
82
|
+
* <root>/openspec/changes/<name> for every root type, including store roots).
|
|
83
|
+
* Missing metadata means "not declared"; a marker that cannot be honored
|
|
84
|
+
* yields invalidReason so callers can say why.
|
|
85
|
+
*/
|
|
86
|
+
export declare function readSkipSpecsMarker(changeDir: string): MetadataMarker;
|
|
87
|
+
/**
|
|
88
|
+
* Non-throwing read of the retire_capabilities marker, with exactly the
|
|
89
|
+
* semantics `readSkipSpecsMarker` documents above.
|
|
90
|
+
*
|
|
91
|
+
* Gates the one archive action that removes a file from `openspec/specs/`: when
|
|
92
|
+
* a change's REMOVED entries take a capability's last requirement, archive
|
|
93
|
+
* deletes the emptied main spec rather than aborting on a spec it cannot write
|
|
94
|
+
* (#1302). Declared rather than inferred because the delete is recoverable only
|
|
95
|
+
* from git, so it is the author's call.
|
|
96
|
+
*/
|
|
97
|
+
export declare function readRetireCapabilitiesMarker(changeDir: string): MetadataMarker;
|
|
55
98
|
//# sourceMappingURL=change-metadata.d.ts.map
|