@fission-ai/openspec 1.6.0-beta.1 → 1.7.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 +28 -1
- package/dist/cli/index.js +73 -7
- package/dist/commands/change.d.ts +0 -1
- package/dist/commands/change.js +90 -50
- 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 +21 -11
- package/dist/commands/show.js +5 -2
- package/dist/commands/spec.js +20 -24
- package/dist/commands/validate.js +18 -3
- 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 +89 -6
- package/dist/commands/workflow/shared.d.ts +13 -2
- package/dist/commands/workflow/shared.js +34 -4
- package/dist/commands/workflow/status.js +7 -2
- package/dist/core/archive.d.ts +0 -1
- package/dist/core/archive.js +96 -33
- package/dist/core/artifact-graph/graph.d.ts +16 -0
- package/dist/core/artifact-graph/graph.js +30 -6
- package/dist/core/artifact-graph/instruction-loader.d.ts +27 -2
- package/dist/core/artifact-graph/instruction-loader.js +55 -7
- package/dist/core/artifact-graph/resolver.d.ts +14 -0
- package/dist/core/artifact-graph/resolver.js +34 -6
- package/dist/core/change-metadata/schema.d.ts +1 -0
- package/dist/core/change-metadata/schema.js +6 -0
- package/dist/core/change-status-policy.d.ts +1 -1
- 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 +8 -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 +7 -1
- package/dist/core/config-schema.js +40 -1
- package/dist/core/config.d.ts +12 -0
- package/dist/core/config.js +21 -3
- package/dist/core/global-config.d.ts +5 -0
- package/dist/core/init.d.ts +35 -0
- package/dist/core/init.js +225 -39
- package/dist/core/legacy-cleanup.d.ts +59 -0
- package/dist/core/legacy-cleanup.js +249 -22
- package/dist/core/list.js +5 -7
- package/dist/core/migration.d.ts +71 -1
- package/dist/core/migration.js +294 -1
- 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 +8 -0
- package/dist/core/parsers/requirement-blocks.js +60 -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.js +3 -29
- package/dist/core/profile-sync-drift.d.ts +0 -8
- package/dist/core/profile-sync-drift.js +9 -45
- package/dist/core/project-config.d.ts +25 -6
- package/dist/core/project-config.js +77 -7
- 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/tool-detection.d.ts +25 -5
- package/dist/core/shared/tool-detection.js +105 -6
- package/dist/core/specs-apply.d.ts +8 -33
- package/dist/core/specs-apply.js +252 -157
- package/dist/core/store/git.d.ts +13 -0
- package/dist/core/store/git.js +24 -0
- package/dist/core/templates/workflows/apply-change.js +54 -4
- package/dist/core/templates/workflows/archive-change.js +166 -42
- package/dist/core/templates/workflows/bulk-archive-change.js +240 -63
- package/dist/core/templates/workflows/continue-change.js +32 -40
- package/dist/core/templates/workflows/explore.js +12 -0
- 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 +4 -4
- package/dist/core/templates/workflows/propose.js +50 -32
- 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 +172 -22
- package/dist/core/templates/workflows/update-change.js +20 -12
- package/dist/core/templates/workflows/verify-change.js +16 -10
- package/dist/core/update.d.ts +39 -5
- package/dist/core/update.js +288 -71
- package/dist/core/validation/constants.d.ts +4 -1
- package/dist/core/validation/constants.js +4 -1
- package/dist/core/validation/validator.d.ts +1 -7
- package/dist/core/validation/validator.js +92 -36
- package/dist/core/version-check.d.ts +152 -0
- package/dist/core/version-check.js +736 -0
- package/dist/core/view.js +12 -17
- package/dist/prompts/searchable-multi-select.js +1 -1
- package/dist/telemetry/index.d.ts +1 -1
- package/dist/telemetry/index.js +55 -33
- package/dist/ui/welcome-screen.d.ts +9 -1
- package/dist/ui/welcome-screen.js +91 -38
- package/dist/utils/change-metadata.d.ts +30 -0
- package/dist/utils/change-metadata.js +101 -7
- package/dist/utils/change-utils.d.ts +10 -4
- package/dist/utils/change-utils.js +20 -13
- package/dist/utils/command-references.d.ts +80 -6
- package/dist/utils/command-references.js +138 -7
- package/dist/utils/date.d.ts +8 -0
- package/dist/utils/date.js +13 -0
- package/dist/utils/index.d.ts +1 -1
- package/dist/utils/index.js +1 -1
- 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 +102 -0
- package/package.json +29 -30
- package/schemas/spec-driven/schema.yaml +60 -5
- package/schemas/spec-driven/templates/design.md +2 -2
- package/schemas/spec-driven/templates/proposal.md +5 -1
- 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/README.md
CHANGED
|
@@ -76,6 +76,29 @@ AI: Archived to openspec/changes/archive/2025-01-23-add-dark-mode/
|
|
|
76
76
|
Specs updated. Ready for the next feature.
|
|
77
77
|
```
|
|
78
78
|
|
|
79
|
+
<details>
|
|
80
|
+
<summary><strong>What do the specs actually look like?</strong></summary>
|
|
81
|
+
|
|
82
|
+
Plain Markdown — requirements with concrete scenarios, no special syntax to learn. Here's what goes in the `specs/` folder created above:
|
|
83
|
+
|
|
84
|
+
```markdown
|
|
85
|
+
## ADDED Requirements
|
|
86
|
+
|
|
87
|
+
### Requirement: Theme selection
|
|
88
|
+
The app SHALL let users switch between light and dark themes,
|
|
89
|
+
defaulting to the system preference.
|
|
90
|
+
|
|
91
|
+
#### Scenario: User toggles dark mode
|
|
92
|
+
- **WHEN** the user clicks the theme toggle
|
|
93
|
+
- **THEN** the app switches to dark mode and persists the choice
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
Your AI writes these; you review the plan before any code is written.
|
|
97
|
+
|
|
98
|
+
OpenSpec is built with OpenSpec — browse this repo's live [specs](openspec/specs) and in-flight [changes](openspec/changes) for real examples at scale.
|
|
99
|
+
|
|
100
|
+
</details>
|
|
101
|
+
|
|
79
102
|
<details>
|
|
80
103
|
<summary><strong>OpenSpec Dashboard</strong></summary>
|
|
81
104
|
|
|
@@ -114,6 +137,8 @@ cd your-project
|
|
|
114
137
|
openspec init
|
|
115
138
|
```
|
|
116
139
|
|
|
140
|
+
> **Want your AI to do it?** Paste the [setup prompt](docs/installation.md#install-with-your-ai-assistant) into your coding assistant — it installs the CLI, runs `openspec init`, and verifies the result.
|
|
141
|
+
|
|
117
142
|
Now talk to your AI:
|
|
118
143
|
|
|
119
144
|
- **Not sure what to build yet?** Start with `/opsx:explore`, a no-stakes thinking partner that reads your code, weighs options, and shapes a plan before anything is written. ([Explore guide](docs/explore.md))
|
|
@@ -121,8 +146,10 @@ Now talk to your AI:
|
|
|
121
146
|
|
|
122
147
|
Both are in the default profile. If you want the expanded workflow (`/opsx:new`, `/opsx:continue`, `/opsx:ff`, `/opsx:verify`, `/opsx:bulk-archive`, `/opsx:onboard`), select it with `openspec config profile` and apply with `openspec update`.
|
|
123
148
|
|
|
149
|
+
`/opsx:propose` is the canonical name; your tool may spell it `/opsx-propose` (Cursor, GitHub Copilot), `@opsx-propose` (Amazon Q) or `$openspec-propose` (Codex). `openspec init` prints the right form for the tools you picked — see [How To Invoke](docs/supported-tools.md#how-to-invoke).
|
|
150
|
+
|
|
124
151
|
> [!NOTE]
|
|
125
|
-
> Not sure if your tool is supported? [View the full list](docs/supported-tools.md) – we support
|
|
152
|
+
> Not sure if your tool is supported? [View the full list](docs/supported-tools.md) – we support 30+ tools and growing.
|
|
126
153
|
>
|
|
127
154
|
> Also works with pnpm, yarn, bun, and nix. [See installation options](docs/installation.md).
|
|
128
155
|
|
package/dist/cli/index.js
CHANGED
|
@@ -5,8 +5,9 @@ import ora from 'ora';
|
|
|
5
5
|
import path from 'path';
|
|
6
6
|
import { fileURLToPath } from 'url';
|
|
7
7
|
import { promises as fs } from 'fs';
|
|
8
|
-
import { AI_TOOLS } from '../core/config.js';
|
|
8
|
+
import { AI_TOOLS, TOOL_ID_ALIASES } from '../core/config.js';
|
|
9
9
|
import { UpdateCommand } from '../core/update.js';
|
|
10
|
+
import { getAvailableCliUpdate, displayCliUpdateNote, shouldOfferUpgrade, getInstallDir, offerCliUpgrade, rerunUpdateWithUpgradedCli, displayUpgradeCommand, isSourceCheckout, } from '../core/version-check.js';
|
|
10
11
|
import { ListCommand } from '../core/list.js';
|
|
11
12
|
import { ArchiveCommand } from '../core/archive.js';
|
|
12
13
|
import { ViewCommand } from '../core/view.js';
|
|
@@ -23,9 +24,10 @@ import { registerStoreCommand } from '../commands/store.js';
|
|
|
23
24
|
import { registerDoctorCommand } from '../commands/doctor.js';
|
|
24
25
|
import { registerContextCommand } from '../commands/context.js';
|
|
25
26
|
import { registerWorksetCommand } from '../commands/workset.js';
|
|
26
|
-
import { statusCommand, instructionsCommand, applyInstructionsCommand, templatesCommand, schemasCommand, newChangeCommand, DEFAULT_SCHEMA, } from '../commands/workflow/index.js';
|
|
27
|
+
import { statusCommand, instructionsCommand, applyInstructionsCommand, archiveInstructionsCommand, templatesCommand, schemasCommand, newChangeCommand, DEFAULT_SCHEMA, } from '../commands/workflow/index.js';
|
|
27
28
|
import { maybeShowTelemetryNotice, trackCommand, shutdown } from '../telemetry/index.js';
|
|
28
29
|
import { COMMON_FLAGS } from '../core/completions/shared-flags.js';
|
|
30
|
+
import { isInteractive } from '../utils/interactive.js';
|
|
29
31
|
const STORE_OPTION_DESCRIPTION = COMMON_FLAGS.store.description;
|
|
30
32
|
// Deliberate rejection path: --store-path stays registered (hidden) so the
|
|
31
33
|
// resolver can explain that registering the path is the supported route,
|
|
@@ -96,13 +98,17 @@ program.hook('postAction', async () => {
|
|
|
96
98
|
await shutdown();
|
|
97
99
|
});
|
|
98
100
|
const availableToolIds = AI_TOOLS.filter((tool) => tool.skillsDir).map((tool) => tool.value);
|
|
99
|
-
const
|
|
101
|
+
const toolAliasNote = Object.entries(TOOL_ID_ALIASES)
|
|
102
|
+
.map(([retired, current]) => `${retired} (now ${current})`)
|
|
103
|
+
.join(', ');
|
|
104
|
+
const toolsOptionDescription = `Configure AI tools non-interactively. Use "all", "none", or a comma-separated list of: ${availableToolIds.join(', ')}. Also accepted: ${toolAliasNote}`;
|
|
100
105
|
program
|
|
101
106
|
.command('init [path]')
|
|
102
107
|
.description('Initialize OpenSpec in your project')
|
|
103
108
|
.option('--tools <tools>', toolsOptionDescription)
|
|
104
109
|
.option('--force', 'Auto-cleanup legacy files without prompting')
|
|
105
110
|
.option('--profile <profile>', 'Override global config profile (core or custom)')
|
|
111
|
+
.option('--no-animation', 'Show a static welcome screen instead of the animated one')
|
|
106
112
|
.action(async (targetPath = '.', options) => {
|
|
107
113
|
try {
|
|
108
114
|
// Validate that the path is a valid directory
|
|
@@ -130,6 +136,7 @@ program
|
|
|
130
136
|
tools: options?.tools,
|
|
131
137
|
force: options?.force,
|
|
132
138
|
profile: options?.profile,
|
|
139
|
+
animation: options?.animation,
|
|
133
140
|
});
|
|
134
141
|
await initCommand.execute(targetPath);
|
|
135
142
|
}
|
|
@@ -165,8 +172,55 @@ program
|
|
|
165
172
|
.option('--force', 'Force update even when tools are up to date')
|
|
166
173
|
.action(async (targetPath = '.', options) => {
|
|
167
174
|
try {
|
|
175
|
+
const installDir = getInstallDir();
|
|
176
|
+
// Running from a clone: the version is whatever the branch says, so any
|
|
177
|
+
// upgrade advice would be noise. Decided before the request, so a
|
|
178
|
+
// contributor never waits on an answer that gets thrown away.
|
|
179
|
+
const latestVersion = isSourceCheckout(installDir) ? null : await getAvailableCliUpdate();
|
|
180
|
+
const announce = latestVersion !== null;
|
|
181
|
+
// Offer to upgrade first: this process generates files from its own
|
|
182
|
+
// templates, so upgrading afterwards would leave the old ones on disk.
|
|
183
|
+
// Both streams must be a terminal — with stdout redirected the question
|
|
184
|
+
// lands in the file and the user waits at a blank screen forever.
|
|
185
|
+
const canOffer = announce &&
|
|
186
|
+
shouldOfferUpgrade({
|
|
187
|
+
installDir,
|
|
188
|
+
projectPath: targetPath,
|
|
189
|
+
interactive: isInteractive(),
|
|
190
|
+
stdoutIsTty: Boolean(process.stdout.isTTY),
|
|
191
|
+
});
|
|
192
|
+
let declined = false;
|
|
193
|
+
if (latestVersion && canOffer) {
|
|
194
|
+
displayCliUpdateNote(latestVersion, targetPath, { withCommand: false });
|
|
195
|
+
const outcome = await offerCliUpgrade(latestVersion);
|
|
196
|
+
// Set the code and return rather than process.exit: exiting here would
|
|
197
|
+
// skip commander's postAction hook, killing the telemetry flush
|
|
198
|
+
// mid-request.
|
|
199
|
+
if (outcome === 'cancelled') {
|
|
200
|
+
// Ctrl-C means stop the command, not fall through to more prompts.
|
|
201
|
+
process.exitCode = 130;
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
if (outcome === 'upgraded') {
|
|
205
|
+
process.exitCode = await rerunUpdateWithUpgradedCli(targetPath, {
|
|
206
|
+
force: options?.force,
|
|
207
|
+
});
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
// Declined, failed, or upgraded-but-unreachable: fall through to the
|
|
211
|
+
// update, then leave the command on screen underneath it.
|
|
212
|
+
declined = true;
|
|
213
|
+
}
|
|
168
214
|
const updateCommand = new UpdateCommand({ force: options?.force });
|
|
169
215
|
await updateCommand.execute(targetPath);
|
|
216
|
+
if (declined) {
|
|
217
|
+
// The headline was printed before the prompt; only the manual route is
|
|
218
|
+
// still owed, and it belongs where the user is looking now.
|
|
219
|
+
displayUpgradeCommand(targetPath);
|
|
220
|
+
}
|
|
221
|
+
else if (latestVersion) {
|
|
222
|
+
displayCliUpdateNote(latestVersion, targetPath);
|
|
223
|
+
}
|
|
170
224
|
}
|
|
171
225
|
catch (error) {
|
|
172
226
|
failWithError(error);
|
|
@@ -212,10 +266,19 @@ program
|
|
|
212
266
|
program
|
|
213
267
|
.command('view')
|
|
214
268
|
.description('Display an interactive dashboard of specs and changes')
|
|
215
|
-
.
|
|
269
|
+
.option('--store <id>', STORE_OPTION_DESCRIPTION)
|
|
270
|
+
.addOption(hiddenStorePathOption())
|
|
271
|
+
.action(async (options) => {
|
|
216
272
|
try {
|
|
273
|
+
// Implicit cwd fallback stays enabled so `view` keeps accepting the same
|
|
274
|
+
// directories as `list`/`status` — notably pre-config.yaml `openspec/`
|
|
275
|
+
// dirs. ViewCommand still reports a missing openspec/ directory itself.
|
|
276
|
+
const root = await resolveRootForCommand(options ?? {});
|
|
277
|
+
if (!root) {
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
217
280
|
const viewCommand = new ViewCommand();
|
|
218
|
-
await viewCommand.execute(
|
|
281
|
+
await viewCommand.execute(root.path);
|
|
219
282
|
}
|
|
220
283
|
catch (error) {
|
|
221
284
|
failWithError(error);
|
|
@@ -460,7 +523,7 @@ program
|
|
|
460
523
|
// Instructions command
|
|
461
524
|
program
|
|
462
525
|
.command('instructions [artifact]')
|
|
463
|
-
.description('Output enriched instructions for
|
|
526
|
+
.description('Output enriched instructions for artifacts, apply, or archive')
|
|
464
527
|
.option('--change <id>', 'Change name')
|
|
465
528
|
.option('--schema <name>', 'Schema override (auto-detected from config.yaml)')
|
|
466
529
|
.option('--json', 'Output as JSON')
|
|
@@ -468,10 +531,13 @@ program
|
|
|
468
531
|
.addOption(hiddenStorePathOption())
|
|
469
532
|
.action(async (artifactId, options) => {
|
|
470
533
|
try {
|
|
471
|
-
//
|
|
534
|
+
// Workflow instruction surfaces are reserved command branches, not artifacts.
|
|
472
535
|
if (artifactId === 'apply') {
|
|
473
536
|
await applyInstructionsCommand(options);
|
|
474
537
|
}
|
|
538
|
+
else if (artifactId === 'archive') {
|
|
539
|
+
await archiveInstructionsCommand(options);
|
|
540
|
+
}
|
|
475
541
|
else {
|
|
476
542
|
await instructionsCommand(artifactId, options);
|
|
477
543
|
}
|
package/dist/commands/change.js
CHANGED
|
@@ -2,12 +2,30 @@ import { promises as fs } from 'fs';
|
|
|
2
2
|
import path from 'path';
|
|
3
3
|
import { JsonConverter } from '../core/converters/json-converter.js';
|
|
4
4
|
import { Validator } from '../core/validation/validator.js';
|
|
5
|
+
import { VALIDATION_MESSAGES } from '../core/validation/constants.js';
|
|
5
6
|
import { ChangeParser } from '../core/parsers/change-parser.js';
|
|
6
7
|
import { isInteractive } from '../utils/interactive.js';
|
|
7
8
|
import { getActiveChangeIds } from '../utils/item-discovery.js';
|
|
8
9
|
import { getTaskProgressForChange } from '../utils/task-progress.js';
|
|
9
|
-
|
|
10
|
-
|
|
10
|
+
/**
|
|
11
|
+
* True only when `target` is definitively absent. An EACCES or I/O failure
|
|
12
|
+
* means existence cannot be determined, so callers fall through to their
|
|
13
|
+
* read-error path rather than claim the file was never written.
|
|
14
|
+
*/
|
|
15
|
+
async function isDefinitelyMissing(target) {
|
|
16
|
+
return fs
|
|
17
|
+
.access(target)
|
|
18
|
+
.then(() => false)
|
|
19
|
+
.catch((error) => error?.code === 'ENOENT');
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* A change is a directory directly under changes/. Rejecting anything else up
|
|
23
|
+
* front keeps a traversing name (`../..`) from reading a proposal outside the
|
|
24
|
+
* changes directory, and keeps the missing-proposal message honest.
|
|
25
|
+
*/
|
|
26
|
+
function isChangeDirectoryName(changesPath, changeDir) {
|
|
27
|
+
return path.dirname(path.resolve(changeDir)) === path.resolve(changesPath);
|
|
28
|
+
}
|
|
11
29
|
export class ChangeCommand {
|
|
12
30
|
converter;
|
|
13
31
|
rootPath;
|
|
@@ -30,7 +48,8 @@ export class ChangeCommand {
|
|
|
30
48
|
const changesPath = this.getChangesPath();
|
|
31
49
|
if (!changeName) {
|
|
32
50
|
const canPrompt = isInteractive(options);
|
|
33
|
-
|
|
51
|
+
// Offer exactly the changes `show <name>` can resolve.
|
|
52
|
+
const changes = await getActiveChangeIds(this.rootPath ?? process.cwd());
|
|
34
53
|
if (canPrompt && changes.length > 0) {
|
|
35
54
|
const { select } = await import('@inquirer/prompts');
|
|
36
55
|
const selected = await select({
|
|
@@ -51,11 +70,29 @@ export class ChangeCommand {
|
|
|
51
70
|
return;
|
|
52
71
|
}
|
|
53
72
|
}
|
|
54
|
-
const
|
|
73
|
+
const changeDir = path.join(changesPath, changeName);
|
|
74
|
+
const proposalPath = path.join(changeDir, 'proposal.md');
|
|
75
|
+
if (!isChangeDirectoryName(changesPath, changeDir)) {
|
|
76
|
+
throw new Error(`Change "${changeName}" not found at ${proposalPath}`);
|
|
77
|
+
}
|
|
55
78
|
try {
|
|
56
79
|
await fs.access(proposalPath);
|
|
57
80
|
}
|
|
58
81
|
catch {
|
|
82
|
+
// A change can exist without a proposal: `openspec new change` scaffolds
|
|
83
|
+
// only .openspec.yaml, and a custom schema need not define a proposal
|
|
84
|
+
// artifact. Say which of the two cases this is instead of reporting a
|
|
85
|
+
// change that does exist as missing. A stray file under changes/ is not a
|
|
86
|
+
// change, and naming it one would point the user at a `status --change`
|
|
87
|
+
// call that cannot work.
|
|
88
|
+
const isChangeDirectory = await fs
|
|
89
|
+
.stat(changeDir)
|
|
90
|
+
.then((stats) => stats.isDirectory())
|
|
91
|
+
.catch(() => false);
|
|
92
|
+
if (isChangeDirectory) {
|
|
93
|
+
throw new Error(`Change "${changeName}" has no proposal.md yet. ` +
|
|
94
|
+
`Run "openspec status --change ${changeName}" to see which artifact comes next.`);
|
|
95
|
+
}
|
|
59
96
|
throw new Error(`Change "${changeName}" not found at ${proposalPath}`);
|
|
60
97
|
}
|
|
61
98
|
if (options?.json) {
|
|
@@ -89,18 +126,30 @@ export class ChangeCommand {
|
|
|
89
126
|
*/
|
|
90
127
|
async list(options) {
|
|
91
128
|
const changesPath = path.join(process.cwd(), 'openspec', 'changes');
|
|
92
|
-
|
|
129
|
+
// Same directory-based resolution as `openspec list`, the command this
|
|
130
|
+
// deprecated alias points users at. Every output path below already
|
|
131
|
+
// tolerates a change whose proposal.md is missing or unreadable.
|
|
132
|
+
const changes = await getActiveChangeIds();
|
|
93
133
|
if (options?.json) {
|
|
94
134
|
const changeDetails = await Promise.all(changes.map(async (changeName) => {
|
|
95
|
-
const
|
|
135
|
+
const changeDir = path.join(changesPath, changeName);
|
|
136
|
+
const proposalPath = path.join(changeDir, 'proposal.md');
|
|
137
|
+
// Resolve task progress through the shared tracked-tasks helper so
|
|
138
|
+
// this deprecated noun-form list cannot re-fork the resolution
|
|
139
|
+
// (#1202). Tasks are independent of the proposal: a change can carry
|
|
140
|
+
// tasks before, or without, a proposal.md.
|
|
141
|
+
const taskStatus = await getTaskProgressForChange(changesPath, changeName, process.cwd());
|
|
142
|
+
// No proposal yet is an ordinary state (scaffolded change, or a
|
|
143
|
+
// schema with no proposal artifact), so name the change rather than
|
|
144
|
+
// labelling it Unknown. Unknown stays for a proposal that exists but
|
|
145
|
+
// cannot be read or parsed.
|
|
146
|
+
if (await isDefinitelyMissing(proposalPath)) {
|
|
147
|
+
return { id: changeName, title: changeName, deltaCount: 0, taskStatus };
|
|
148
|
+
}
|
|
96
149
|
try {
|
|
97
150
|
const content = await fs.readFile(proposalPath, 'utf-8');
|
|
98
|
-
const changeDir = path.join(changesPath, changeName);
|
|
99
151
|
const parser = new ChangeParser(content, changeDir);
|
|
100
152
|
const change = await parser.parseChangeWithDeltas(changeName);
|
|
101
|
-
// Resolve task progress through the shared tracked-tasks helper so
|
|
102
|
-
// this deprecated noun-form list cannot re-fork the resolution (#1202).
|
|
103
|
-
const taskStatus = await getTaskProgressForChange(changesPath, changeName, process.cwd());
|
|
104
153
|
return {
|
|
105
154
|
id: changeName,
|
|
106
155
|
title: this.extractTitle(content, changeName),
|
|
@@ -108,13 +157,8 @@ export class ChangeCommand {
|
|
|
108
157
|
taskStatus,
|
|
109
158
|
};
|
|
110
159
|
}
|
|
111
|
-
catch
|
|
112
|
-
return {
|
|
113
|
-
id: changeName,
|
|
114
|
-
title: 'Unknown',
|
|
115
|
-
deltaCount: 0,
|
|
116
|
-
taskStatus: { total: 0, completed: 0 },
|
|
117
|
-
};
|
|
160
|
+
catch {
|
|
161
|
+
return { id: changeName, title: 'Unknown', deltaCount: 0, taskStatus };
|
|
118
162
|
}
|
|
119
163
|
}));
|
|
120
164
|
const sorted = changeDetails.sort((a, b) => a.id.localeCompare(b.id));
|
|
@@ -133,20 +177,24 @@ export class ChangeCommand {
|
|
|
133
177
|
}
|
|
134
178
|
// Long format: id: title and minimal counts
|
|
135
179
|
for (const changeName of sorted) {
|
|
136
|
-
const
|
|
180
|
+
const changeDir = path.join(changesPath, changeName);
|
|
181
|
+
const proposalPath = path.join(changeDir, 'proposal.md');
|
|
182
|
+
const { total, completed } = await getTaskProgressForChange(changesPath, changeName, process.cwd());
|
|
183
|
+
const taskStatusText = total > 0 ? ` [tasks ${completed}/${total}]` : '';
|
|
184
|
+
if (await isDefinitelyMissing(proposalPath)) {
|
|
185
|
+
console.log(`${changeName}: (no proposal.md yet)${taskStatusText}`);
|
|
186
|
+
continue;
|
|
187
|
+
}
|
|
137
188
|
try {
|
|
138
189
|
const content = await fs.readFile(proposalPath, 'utf-8');
|
|
139
190
|
const title = this.extractTitle(content, changeName);
|
|
140
|
-
const
|
|
141
|
-
const taskStatusText = total > 0 ? ` [tasks ${completed}/${total}]` : '';
|
|
142
|
-
const changeDir = path.join(changesPath, changeName);
|
|
143
|
-
const parser = new ChangeParser(await fs.readFile(proposalPath, 'utf-8'), changeDir);
|
|
191
|
+
const parser = new ChangeParser(content, changeDir);
|
|
144
192
|
const change = await parser.parseChangeWithDeltas(changeName);
|
|
145
193
|
const deltaCountText = ` [deltas ${change.deltas.length}]`;
|
|
146
194
|
console.log(`${changeName}: ${title}${deltaCountText}${taskStatusText}`);
|
|
147
195
|
}
|
|
148
196
|
catch {
|
|
149
|
-
console.log(`${changeName}: (unable to read)`);
|
|
197
|
+
console.log(`${changeName}: (unable to read)${taskStatusText}`);
|
|
150
198
|
}
|
|
151
199
|
}
|
|
152
200
|
}
|
|
@@ -200,44 +248,36 @@ export class ChangeCommand {
|
|
|
200
248
|
console.error(`${prefix} [${label}] ${issue.path}: ${issue.message}`);
|
|
201
249
|
});
|
|
202
250
|
// Next steps footer to guide fixing issues
|
|
203
|
-
this.printNextSteps();
|
|
251
|
+
this.printNextSteps(report.issues);
|
|
204
252
|
if (!options?.json) {
|
|
205
253
|
process.exitCode = 1;
|
|
206
254
|
}
|
|
207
255
|
}
|
|
208
256
|
}
|
|
209
257
|
}
|
|
210
|
-
async getActiveChanges(changesPath) {
|
|
211
|
-
try {
|
|
212
|
-
const entries = await fs.readdir(changesPath, { withFileTypes: true });
|
|
213
|
-
const result = [];
|
|
214
|
-
for (const entry of entries) {
|
|
215
|
-
if (!entry.isDirectory() || entry.name.startsWith('.') || entry.name === ARCHIVE_DIR)
|
|
216
|
-
continue;
|
|
217
|
-
const proposalPath = path.join(changesPath, entry.name, 'proposal.md');
|
|
218
|
-
try {
|
|
219
|
-
await fs.access(proposalPath);
|
|
220
|
-
result.push(entry.name);
|
|
221
|
-
}
|
|
222
|
-
catch {
|
|
223
|
-
// skip directories without proposal.md
|
|
224
|
-
}
|
|
225
|
-
}
|
|
226
|
-
return result.sort();
|
|
227
|
-
}
|
|
228
|
-
catch {
|
|
229
|
-
return [];
|
|
230
|
-
}
|
|
231
|
-
}
|
|
232
258
|
extractTitle(content, changeName) {
|
|
233
259
|
const match = content.match(/^#\s+(?:Change:\s+)?(.+)$/im);
|
|
234
260
|
return match ? match[1].trim() : changeName;
|
|
235
261
|
}
|
|
236
|
-
printNextSteps() {
|
|
262
|
+
printNextSteps(issues = []) {
|
|
237
263
|
const bullets = [];
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
264
|
+
// Branch on the exact marker messages: the generic no-deltas guidance
|
|
265
|
+
// also mentions skip_specs and must not trigger the marker bullets.
|
|
266
|
+
const conflictIssue = issues.some(i => i.message.includes(VALIDATION_MESSAGES.CHANGE_SKIP_SPECS_CONFLICT));
|
|
267
|
+
const invalidMarkerIssue = issues.some(i => i.message.includes(VALIDATION_MESSAGES.CHANGE_SKIP_SPECS_INVALID_METADATA));
|
|
268
|
+
if (conflictIssue) {
|
|
269
|
+
bullets.push('- This change declares skip_specs (no spec deltas): delete the files under specs/, or remove skip_specs from .openspec.yaml if requirements do change');
|
|
270
|
+
bullets.push('- skip_specs is only honored when .openspec.yaml is valid change metadata (schema: <name> is required)');
|
|
271
|
+
}
|
|
272
|
+
else if (invalidMarkerIssue) {
|
|
273
|
+
bullets.push('- Fix .openspec.yaml so the skip_specs marker can be honored (schema: <name> is required)');
|
|
274
|
+
bullets.push('- Or remove skip_specs from .openspec.yaml and add delta specs instead');
|
|
275
|
+
}
|
|
276
|
+
else {
|
|
277
|
+
bullets.push('- Ensure change has deltas in specs/: use headers ## ADDED/MODIFIED/REMOVED/RENAMED Requirements');
|
|
278
|
+
bullets.push('- Each requirement MUST include at least one #### Scenario: block');
|
|
279
|
+
bullets.push('- Debug parsed deltas: openspec change show <id> --json --deltas-only');
|
|
280
|
+
}
|
|
241
281
|
console.error('Next steps:');
|
|
242
282
|
bullets.forEach(b => console.error(` ${b}`));
|
|
243
283
|
}
|
package/dist/commands/config.js
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
|
-
import { spawn
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
2
|
import * as fs from 'node:fs';
|
|
3
3
|
import * as path from 'node:path';
|
|
4
4
|
import { getGlobalConfigPath, getGlobalConfig, saveGlobalConfig, } from '../core/global-config.js';
|
|
5
|
-
import { getNestedValue, setNestedValue, deleteNestedValue, coerceValue, formatValueYaml, validateConfigKeyPath, validateConfig, DEFAULT_CONFIG, } from '../core/config-schema.js';
|
|
5
|
+
import { getNestedValue, setNestedValue, deleteNestedValue, coerceValue, formatValueYaml, validateConfigKeyPath, hasUnsafeKeySegment, validateConfig, DEFAULT_CONFIG, } from '../core/config-schema.js';
|
|
6
6
|
import { CORE_WORKFLOWS, ALL_WORKFLOWS, getProfileWorkflows } from '../core/profiles.js';
|
|
7
7
|
import { OPENSPEC_DIR_NAME } from '../core/config.js';
|
|
8
8
|
import { hasProjectConfigDrift } from '../core/profile-sync-drift.js';
|
|
9
|
-
import {
|
|
9
|
+
import { UpdateCommand } from '../core/update.js';
|
|
10
|
+
import { asErrorMessage, isPromptCancellationError } from './shared-output.js';
|
|
10
11
|
const WORKFLOW_PROMPT_META = {
|
|
11
12
|
propose: {
|
|
12
13
|
name: 'Propose change',
|
|
@@ -233,11 +234,15 @@ export function registerConfigCommand(program) {
|
|
|
233
234
|
.action((key, value, options) => {
|
|
234
235
|
const allowUnknown = Boolean(options.allowUnknown);
|
|
235
236
|
const keyValidation = validateConfigKeyPath(key);
|
|
236
|
-
|
|
237
|
+
// --allow-unknown relaxes the known-key check, but never the prototype-safety check.
|
|
238
|
+
const unsafeKey = hasUnsafeKeySegment(key);
|
|
239
|
+
if (!keyValidation.valid && (!allowUnknown || unsafeKey)) {
|
|
237
240
|
const reason = keyValidation.reason ? ` ${keyValidation.reason}.` : '';
|
|
238
241
|
console.error(`Error: Invalid configuration key "${key}".${reason}`);
|
|
239
242
|
console.error('Use "openspec config list" to see available keys.');
|
|
240
|
-
|
|
243
|
+
if (!allowUnknown && !unsafeKey) {
|
|
244
|
+
console.error('Pass --allow-unknown to bypass this check.');
|
|
245
|
+
}
|
|
241
246
|
process.exitCode = 1;
|
|
242
247
|
return;
|
|
243
248
|
}
|
|
@@ -527,11 +532,12 @@ export function registerConfigCommand(program) {
|
|
|
527
532
|
});
|
|
528
533
|
if (applyNow) {
|
|
529
534
|
try {
|
|
530
|
-
|
|
535
|
+
await new UpdateCommand().execute(projectDir);
|
|
531
536
|
console.log('Run `openspec update` in your other projects to apply.');
|
|
532
537
|
}
|
|
533
|
-
catch {
|
|
534
|
-
console.error(
|
|
538
|
+
catch (error) {
|
|
539
|
+
console.error(`\`openspec update\` failed: ${asErrorMessage(error)}`);
|
|
540
|
+
console.error('Please run it manually to apply the profile changes.');
|
|
535
541
|
process.exitCode = 1;
|
|
536
542
|
}
|
|
537
543
|
return;
|
package/dist/commands/doctor.js
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
import { Option } from 'commander';
|
|
7
7
|
import { resolveRootForCommand, } from '../core/root-selection.js';
|
|
8
8
|
import { readOptionalStoreMetadataState } from '../core/store/foundation.js';
|
|
9
|
-
import { gitOriginUrl, isGitRepositoryAtRoot } from '../core/store/git.js';
|
|
9
|
+
import { gitOriginUrl, gitTrackingDrift, isGitRepositoryAtRoot } from '../core/store/git.js';
|
|
10
10
|
import { classifyOpenSpecDir, readProjectConfig, resolveConfigFilePath, } from '../core/project-config.js';
|
|
11
11
|
import { findRepoPlanningRootSync } from '../core/planning-home.js';
|
|
12
12
|
import { gatherRelationshipData } from './shared-gather.js';
|
|
@@ -27,7 +27,8 @@ async function gatherHealth(root) {
|
|
|
27
27
|
referenceEntries,
|
|
28
28
|
registryUnreadable,
|
|
29
29
|
};
|
|
30
|
-
// Store facts for store-backed roots (explicit --store
|
|
30
|
+
// Store facts for store-backed roots (explicit --store, a declared
|
|
31
|
+
// pointer, or the global default).
|
|
31
32
|
// Missing/invalid metadata never reaches here: store resolution
|
|
32
33
|
// verifies identity first and fails with the existing taxonomy
|
|
33
34
|
// (recorded amendment - corrupt store.yaml is an exit-1 resolution
|
|
@@ -35,14 +36,18 @@ async function gatherHealth(root) {
|
|
|
35
36
|
if (root.storeId) {
|
|
36
37
|
const metadata = await readOptionalStoreMetadataState(root.path).catch(() => null);
|
|
37
38
|
// git -C walks UP the tree: probing a non-repo store nested inside
|
|
38
|
-
// another repo would record the ENCLOSING repo's origin.
|
|
39
|
-
const
|
|
39
|
+
// another repo would record the ENCLOSING repo's origin (and drift).
|
|
40
|
+
const isRepo = await isGitRepositoryAtRoot(root.path);
|
|
41
|
+
const [originUrl, drift] = isRepo
|
|
42
|
+
? await Promise.all([gitOriginUrl(root.path), gitTrackingDrift(root.path)])
|
|
43
|
+
: [null, null];
|
|
40
44
|
input.storeFacts = {
|
|
41
45
|
id: root.storeId,
|
|
42
46
|
metadataPresent: metadata !== null,
|
|
43
47
|
metadataValid: metadata !== null,
|
|
44
48
|
...(metadata?.remote ? { canonicalRemote: metadata.remote } : {}),
|
|
45
49
|
...(originUrl ? { originUrl } : {}),
|
|
50
|
+
...(drift ? { drift } : {}),
|
|
46
51
|
};
|
|
47
52
|
}
|
|
48
53
|
// The 3.2 both-shapes wrong turn, structured — including a malformed
|
|
@@ -105,37 +105,88 @@ function displayFormattedFeedback(title, body) {
|
|
|
105
105
|
console.log('--- END FEEDBACK ---\n');
|
|
106
106
|
}
|
|
107
107
|
/**
|
|
108
|
-
*
|
|
108
|
+
* Check whether gh refused the issue because the repository does not define
|
|
109
|
+
* the label. gh resolves label names before creating the issue, so this
|
|
110
|
+
* failure means no issue was created.
|
|
111
|
+
*
|
|
112
|
+
* Only gh's stderr is inspected. The error message also embeds the command
|
|
113
|
+
* line, which carries the user's own feedback text.
|
|
114
|
+
*/
|
|
115
|
+
function isMissingLabelError(error) {
|
|
116
|
+
return /could not add label/i.test(error?.stderr?.toString() ?? '');
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Report a gh CLI failure and exit, preserving gh's exit code.
|
|
120
|
+
*
|
|
121
|
+
* gh failed after the user already typed their feedback (issues disabled,
|
|
122
|
+
* network, rate limit, ...), so show the same manual-submission path the
|
|
123
|
+
* missing-gh and unauthenticated flows get instead of discarding the text.
|
|
124
|
+
*/
|
|
125
|
+
function reportGhFailure(error, title, body) {
|
|
126
|
+
// Display the error output from gh CLI
|
|
127
|
+
if (error.stderr) {
|
|
128
|
+
console.error(error.stderr.toString());
|
|
129
|
+
}
|
|
130
|
+
else if (error.message) {
|
|
131
|
+
console.error(error.message);
|
|
132
|
+
}
|
|
133
|
+
displayFormattedFeedback(title, body);
|
|
134
|
+
const manualUrl = generateManualSubmissionUrl(title, body);
|
|
135
|
+
console.log('Please submit your feedback manually:');
|
|
136
|
+
console.log(manualUrl);
|
|
137
|
+
// Exit with the same code as gh CLI
|
|
138
|
+
process.exit(error.status ?? 1);
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Create the feedback issue via gh CLI
|
|
109
142
|
* Uses execFileSync to prevent shell injection vulnerabilities
|
|
110
143
|
*/
|
|
144
|
+
function createIssue(title, body, labels) {
|
|
145
|
+
const args = [
|
|
146
|
+
'issue',
|
|
147
|
+
'create',
|
|
148
|
+
'--repo',
|
|
149
|
+
'Fission-AI/OpenSpec',
|
|
150
|
+
'--title',
|
|
151
|
+
title,
|
|
152
|
+
'--body',
|
|
153
|
+
body,
|
|
154
|
+
];
|
|
155
|
+
for (const label of labels) {
|
|
156
|
+
args.push('--label', label);
|
|
157
|
+
}
|
|
158
|
+
const result = execFileSync('gh', args, { encoding: 'utf-8', stdio: 'pipe' });
|
|
159
|
+
return result.trim();
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* Submit feedback via gh CLI
|
|
163
|
+
*/
|
|
111
164
|
function submitViaGhCli(title, body) {
|
|
165
|
+
let issueUrl;
|
|
166
|
+
let labelApplied = true;
|
|
112
167
|
try {
|
|
113
|
-
|
|
114
|
-
'issue',
|
|
115
|
-
'create',
|
|
116
|
-
'--repo',
|
|
117
|
-
'Fission-AI/OpenSpec',
|
|
118
|
-
'--title',
|
|
119
|
-
title,
|
|
120
|
-
'--body',
|
|
121
|
-
body,
|
|
122
|
-
'--label',
|
|
123
|
-
'feedback',
|
|
124
|
-
], { encoding: 'utf-8', stdio: 'pipe' });
|
|
125
|
-
const issueUrl = result.trim();
|
|
126
|
-
console.log(`\n✓ Feedback submitted successfully!`);
|
|
127
|
-
console.log(`Issue URL: ${issueUrl}\n`);
|
|
168
|
+
issueUrl = createIssue(title, body, ['feedback']);
|
|
128
169
|
}
|
|
129
170
|
catch (error) {
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
171
|
+
if (!isMissingLabelError(error)) {
|
|
172
|
+
reportGhFailure(error, title, body);
|
|
173
|
+
return;
|
|
133
174
|
}
|
|
134
|
-
|
|
135
|
-
|
|
175
|
+
// The repository does not define the 'feedback' label. Nothing was
|
|
176
|
+
// created, so retry unlabeled rather than dropping the feedback.
|
|
177
|
+
try {
|
|
178
|
+
issueUrl = createIssue(title, body, []);
|
|
179
|
+
labelApplied = false;
|
|
136
180
|
}
|
|
137
|
-
|
|
138
|
-
|
|
181
|
+
catch (retryError) {
|
|
182
|
+
reportGhFailure(retryError, title, body);
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
console.log(`\n✓ Feedback submitted successfully!`);
|
|
187
|
+
console.log(`Issue URL: ${issueUrl}\n`);
|
|
188
|
+
if (!labelApplied) {
|
|
189
|
+
console.log("Note: created without the 'feedback' label because the repository does not define it.\n");
|
|
139
190
|
}
|
|
140
191
|
}
|
|
141
192
|
/**
|