@fission-ai/openspec 1.4.1 → 1.5.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 +24 -3
- package/dist/cli/index.d.ts +5 -0
- package/dist/cli/index.js +95 -95
- package/dist/commands/change.d.ts +5 -1
- package/dist/commands/change.js +17 -15
- package/dist/commands/config.js +6 -65
- package/dist/commands/context.d.ts +3 -0
- package/dist/commands/context.js +155 -0
- package/dist/commands/doctor.d.ts +8 -0
- package/dist/commands/doctor.js +163 -0
- package/dist/commands/shared-gather.d.ts +14 -0
- package/dist/commands/shared-gather.js +31 -0
- package/dist/commands/shared-output.d.ts +18 -0
- package/dist/commands/shared-output.js +61 -0
- package/dist/commands/show.d.ts +11 -6
- package/dist/commands/show.js +70 -25
- package/dist/commands/spec.d.ts +5 -1
- package/dist/commands/spec.js +15 -4
- package/dist/commands/store.d.ts +3 -0
- package/dist/commands/store.js +547 -0
- package/dist/commands/validate.d.ts +2 -0
- package/dist/commands/validate.js +75 -39
- package/dist/commands/workflow/index.d.ts +0 -2
- package/dist/commands/workflow/index.js +0 -1
- package/dist/commands/workflow/instructions.d.ts +11 -1
- package/dist/commands/workflow/instructions.js +71 -22
- package/dist/commands/workflow/new-change.d.ts +6 -3
- package/dist/commands/workflow/new-change.js +42 -77
- package/dist/commands/workflow/shared.d.ts +15 -3
- package/dist/commands/workflow/shared.js +20 -3
- package/dist/commands/workflow/status.d.ts +2 -0
- package/dist/commands/workflow/status.js +19 -17
- package/dist/commands/workset-input.d.ts +19 -0
- package/dist/commands/workset-input.js +112 -0
- package/dist/commands/workset-prompts.d.ts +12 -0
- package/dist/commands/workset-prompts.js +143 -0
- package/dist/commands/workset.d.ts +25 -0
- package/dist/commands/workset.js +446 -0
- package/dist/core/archive.d.ts +17 -6
- package/dist/core/archive.js +209 -56
- package/dist/core/artifact-graph/index.d.ts +1 -1
- package/dist/core/artifact-graph/instruction-loader.d.ts +18 -13
- package/dist/core/artifact-graph/instruction-loader.js +8 -31
- package/dist/core/change-metadata/schema.d.ts +1 -0
- package/dist/core/change-metadata/schema.js +4 -2
- package/dist/core/change-status-policy.d.ts +5 -18
- package/dist/core/change-status-policy.js +2 -37
- package/dist/core/command-generation/adapters/bob.js +1 -14
- package/dist/core/command-generation/adapters/claude.js +1 -14
- package/dist/core/command-generation/adapters/cursor.js +1 -14
- package/dist/core/command-generation/adapters/pi.js +1 -14
- package/dist/core/command-generation/adapters/windsurf.js +1 -14
- package/dist/core/command-generation/yaml.d.ts +22 -0
- package/dist/core/command-generation/yaml.js +38 -0
- package/dist/core/completions/command-registry.js +87 -289
- package/dist/core/completions/generators/zsh-generator.js +3 -1
- package/dist/core/completions/installers/bash-installer.js +3 -0
- package/dist/core/completions/installers/fish-installer.js +8 -0
- package/dist/core/completions/installers/powershell-installer.js +30 -2
- package/dist/core/completions/installers/zsh-installer.js +3 -0
- package/dist/core/completions/shared-flags.d.ts +1 -0
- package/dist/core/completions/shared-flags.js +5 -0
- package/dist/core/config-schema.d.ts +2 -1
- package/dist/core/config-schema.js +26 -0
- package/dist/core/file-state.d.ts +36 -0
- package/dist/core/file-state.js +112 -0
- package/dist/core/global-config.d.ts +2 -0
- package/dist/core/id.d.ts +17 -0
- package/dist/core/id.js +30 -0
- package/dist/core/index.d.ts +3 -3
- package/dist/core/index.js +3 -3
- package/dist/core/init.js +24 -4
- package/dist/core/list.d.ts +2 -0
- package/dist/core/list.js +19 -5
- package/dist/core/openers.d.ts +77 -0
- package/dist/core/openers.js +251 -0
- package/dist/core/openspec-root.d.ts +45 -0
- package/dist/core/openspec-root.js +192 -0
- package/dist/core/planning-home.d.ts +1 -6
- package/dist/core/planning-home.js +2 -43
- package/dist/core/project-config.d.ts +44 -1
- package/dist/core/project-config.js +160 -10
- package/dist/core/references.d.ts +63 -0
- package/dist/core/references.js +310 -0
- package/dist/core/relationship-health.d.ts +65 -0
- package/dist/core/relationship-health.js +64 -0
- package/dist/core/root-selection.d.ts +122 -0
- package/dist/core/root-selection.js +337 -0
- package/dist/core/specs-apply.d.ts +6 -1
- package/dist/core/specs-apply.js +6 -4
- package/dist/core/store/errors.d.ts +20 -0
- package/dist/core/{context-store → store}/errors.js +3 -3
- package/dist/core/store/foundation.d.ts +56 -0
- package/dist/core/store/foundation.js +251 -0
- package/dist/core/store/git.d.ts +23 -0
- package/dist/core/store/git.js +137 -0
- package/dist/core/{context-store → store}/index.d.ts +0 -1
- package/dist/core/{context-store → store}/index.js +0 -1
- package/dist/core/store/operations.d.ts +114 -0
- package/dist/core/store/operations.js +783 -0
- package/dist/core/store/registry.d.ts +58 -0
- package/dist/core/store/registry.js +275 -0
- package/dist/core/templates/workflows/apply-change.js +5 -4
- package/dist/core/templates/workflows/archive-change.js +5 -4
- package/dist/core/templates/workflows/bulk-archive-change.js +5 -4
- package/dist/core/templates/workflows/continue-change.js +7 -2
- package/dist/core/templates/workflows/explore.js +5 -0
- package/dist/core/templates/workflows/ff-change.js +5 -0
- package/dist/core/templates/workflows/new-change.js +5 -0
- package/dist/core/templates/workflows/onboard.js +4 -1
- package/dist/core/templates/workflows/propose.js +5 -0
- package/dist/core/templates/workflows/store-selection.d.ts +8 -0
- package/dist/core/templates/workflows/store-selection.js +8 -0
- package/dist/core/templates/workflows/sync-specs.js +5 -4
- package/dist/core/templates/workflows/verify-change.js +5 -4
- package/dist/core/working-set.d.ts +47 -0
- package/dist/core/working-set.js +43 -0
- package/dist/core/worksets.d.ts +75 -0
- package/dist/core/worksets.js +245 -0
- package/dist/core/zod-issues.d.ts +4 -0
- package/dist/core/zod-issues.js +10 -0
- package/dist/utils/change-metadata.d.ts +1 -0
- package/dist/utils/change-metadata.js +1 -1
- package/dist/utils/change-utils.js +15 -0
- package/dist/utils/file-system.js +36 -17
- package/package.json +6 -6
- package/dist/commands/context-store.d.ts +0 -3
- package/dist/commands/context-store.js +0 -475
- package/dist/commands/initiative.d.ts +0 -13
- package/dist/commands/initiative.js +0 -318
- package/dist/commands/workflow/initiative-link.d.ts +0 -24
- package/dist/commands/workflow/initiative-link.js +0 -47
- package/dist/commands/workflow/set-change.d.ts +0 -13
- package/dist/commands/workflow/set-change.js +0 -87
- package/dist/commands/workspace/context-status.d.ts +0 -4
- package/dist/commands/workspace/context-status.js +0 -59
- package/dist/commands/workspace/open-target-selection.d.ts +0 -13
- package/dist/commands/workspace/open-target-selection.js +0 -146
- package/dist/commands/workspace/open-view.d.ts +0 -62
- package/dist/commands/workspace/open-view.js +0 -249
- package/dist/commands/workspace/open.d.ts +0 -37
- package/dist/commands/workspace/open.js +0 -110
- package/dist/commands/workspace/opener-selection.d.ts +0 -11
- package/dist/commands/workspace/opener-selection.js +0 -98
- package/dist/commands/workspace/operations.d.ts +0 -29
- package/dist/commands/workspace/operations.js +0 -543
- package/dist/commands/workspace/prompt-theme.d.ts +0 -29
- package/dist/commands/workspace/prompt-theme.js +0 -24
- package/dist/commands/workspace/registration.d.ts +0 -13
- package/dist/commands/workspace/registration.js +0 -84
- package/dist/commands/workspace/selection.d.ts +0 -8
- package/dist/commands/workspace/selection.js +0 -104
- package/dist/commands/workspace/setup-prompts.d.ts +0 -13
- package/dist/commands/workspace/setup-prompts.js +0 -121
- package/dist/commands/workspace/types.d.ts +0 -103
- package/dist/commands/workspace/types.js +0 -36
- package/dist/commands/workspace.d.ts +0 -5
- package/dist/commands/workspace.js +0 -574
- package/dist/core/collections/index.d.ts +0 -3
- package/dist/core/collections/index.js +0 -3
- package/dist/core/collections/initiatives/collection.d.ts +0 -4
- package/dist/core/collections/initiatives/collection.js +0 -17
- package/dist/core/collections/initiatives/index.d.ts +0 -6
- package/dist/core/collections/initiatives/index.js +0 -6
- package/dist/core/collections/initiatives/operations.d.ts +0 -49
- package/dist/core/collections/initiatives/operations.js +0 -175
- package/dist/core/collections/initiatives/resolution.d.ts +0 -87
- package/dist/core/collections/initiatives/resolution.js +0 -374
- package/dist/core/collections/initiatives/schema.d.ts +0 -41
- package/dist/core/collections/initiatives/schema.js +0 -134
- package/dist/core/collections/initiatives/templates.d.ts +0 -12
- package/dist/core/collections/initiatives/templates.js +0 -90
- package/dist/core/collections/runtime.d.ts +0 -46
- package/dist/core/collections/runtime.js +0 -194
- package/dist/core/context-store/binding.d.ts +0 -53
- package/dist/core/context-store/binding.js +0 -197
- package/dist/core/context-store/errors.d.ts +0 -20
- package/dist/core/context-store/foundation.d.ts +0 -55
- package/dist/core/context-store/foundation.js +0 -321
- package/dist/core/context-store/operations.d.ts +0 -85
- package/dist/core/context-store/operations.js +0 -528
- package/dist/core/context-store/registry.d.ts +0 -45
- package/dist/core/context-store/registry.js +0 -229
- package/dist/core/workspace/foundation.d.ts +0 -67
- package/dist/core/workspace/foundation.js +0 -295
- package/dist/core/workspace/index.d.ts +0 -8
- package/dist/core/workspace/index.js +0 -8
- package/dist/core/workspace/legacy-state.d.ts +0 -28
- package/dist/core/workspace/legacy-state.js +0 -200
- package/dist/core/workspace/link-input.d.ts +0 -9
- package/dist/core/workspace/link-input.js +0 -32
- package/dist/core/workspace/open-surface.d.ts +0 -45
- package/dist/core/workspace/open-surface.js +0 -215
- package/dist/core/workspace/openers.d.ts +0 -21
- package/dist/core/workspace/openers.js +0 -124
- package/dist/core/workspace/registry.d.ts +0 -24
- package/dist/core/workspace/registry.js +0 -146
- package/dist/core/workspace/skills.d.ts +0 -57
- package/dist/core/workspace/skills.js +0 -334
- package/dist/core/workspace/state-io.d.ts +0 -10
- package/dist/core/workspace/state-io.js +0 -121
- package/schemas/workspace-planning/schema.yaml +0 -72
- package/schemas/workspace-planning/templates/design.md +0 -33
- package/schemas/workspace-planning/templates/proposal.md +0 -28
- package/schemas/workspace-planning/templates/spec.md +0 -9
- package/schemas/workspace-planning/templates/tasks.md +0 -15
|
@@ -1,62 +1,37 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* New Change Command
|
|
3
3
|
*
|
|
4
|
-
* Creates a new change directory with optional description and schema
|
|
4
|
+
* Creates a new change directory with optional description and schema in the
|
|
5
|
+
* resolved OpenSpec root. `--store <id>` selects a registered store's
|
|
6
|
+
* root; initiative linking and workspace affected areas are no longer part of
|
|
7
|
+
* this command.
|
|
5
8
|
*/
|
|
6
9
|
import ora from 'ora';
|
|
7
10
|
import path from 'path';
|
|
8
11
|
import { createChange, validateChangeName } from '../../utils/change-utils.js';
|
|
9
|
-
import { formatChangeLocation
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
import { assertInitiativeSelectorsHaveReference, assertRepoLocalInitiativeLinkPlanningHome, formatInitiativeLink, printJson, statusFromError, } from './initiative-link.js';
|
|
12
|
+
import { formatChangeLocation } from '../../core/planning-home.js';
|
|
13
|
+
import { resolveRootForCommand, RootSelectionError, toPlanningHome, toRootOutput, withStoreFlag, isStoreSelectedRoot, } from '../../core/root-selection.js';
|
|
14
|
+
import { printJson, statusFromError, validateSchemaExists } from './shared.js';
|
|
13
15
|
// -----------------------------------------------------------------------------
|
|
14
16
|
// Command Implementation
|
|
15
17
|
// -----------------------------------------------------------------------------
|
|
16
|
-
function
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
.map((area) => area.trim())
|
|
20
|
-
.filter((area) => area.length > 0);
|
|
21
|
-
}
|
|
22
|
-
function validateWorkspaceAffectedAreas(planningHome, affectedAreas) {
|
|
23
|
-
if (affectedAreas.length === 0) {
|
|
24
|
-
return;
|
|
25
|
-
}
|
|
26
|
-
if (planningHome.kind !== 'workspace') {
|
|
27
|
-
throw new Error('--areas can only be used when creating a workspace-scoped change');
|
|
18
|
+
function assertRemovedOptionsAbsent(options) {
|
|
19
|
+
if (options.initiative !== undefined) {
|
|
20
|
+
throw new RootSelectionError('--initiative is no longer supported. Normal changes no longer attach to initiatives; --store <id> selects the OpenSpec root.', 'initiative_option_removed', { target: 'change.options' });
|
|
28
21
|
}
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
if (invalidAreas.length > 0) {
|
|
32
|
-
const validList = [...validAreas].sort((a, b) => a.localeCompare(b));
|
|
33
|
-
const validMessage = validList.length > 0 ? validList.join(', ') : '(no registered links)';
|
|
34
|
-
throw new Error(`Invalid affected area${invalidAreas.length === 1 ? '' : 's'}: ${invalidAreas.join(', ')}. ` +
|
|
35
|
-
`Valid workspace link names: ${validMessage}`);
|
|
22
|
+
if (options.areas !== undefined) {
|
|
23
|
+
throw new RootSelectionError('--areas is no longer supported. Workspace affected areas are not part of the normal OpenSpec root path.', 'areas_option_removed', { target: 'change.options' });
|
|
36
24
|
}
|
|
37
25
|
}
|
|
38
|
-
function
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
},
|
|
46
|
-
...(initiative ? { initiative } : {}),
|
|
47
|
-
};
|
|
48
|
-
}
|
|
49
|
-
function printCreatedChangeHuman(payload, planningHome) {
|
|
50
|
-
if (!payload.change) {
|
|
51
|
-
return;
|
|
52
|
-
}
|
|
53
|
-
const location = formatChangeLocation(planningHome, payload.change.id);
|
|
54
|
-
const scope = planningHome.kind === 'workspace' ? 'workspace change' : 'change';
|
|
55
|
-
console.log(`Created ${scope} '${payload.change.id}' at ${location}/`);
|
|
26
|
+
function printCreatedChangeHuman(payload, root) {
|
|
27
|
+
// A relative path is only honest when the root is where the user
|
|
28
|
+
// stands; a distant ancestor root gets the absolute path.
|
|
29
|
+
const location = !isStoreSelectedRoot(root) && root.path === process.cwd()
|
|
30
|
+
? formatChangeLocation(toPlanningHome(root), payload.change.id)
|
|
31
|
+
: payload.change.path;
|
|
32
|
+
console.log(`Created change '${payload.change.id}' at ${location}/`);
|
|
56
33
|
console.log(`Schema: ${payload.change.schema}`);
|
|
57
|
-
|
|
58
|
-
console.log(`Initiative: ${formatInitiativeLink(payload.initiative)}`);
|
|
59
|
-
}
|
|
34
|
+
console.log(`Next: ${withStoreFlag(root, `openspec status --change ${payload.change.id}`)}`);
|
|
60
35
|
}
|
|
61
36
|
export async function newChangeCommand(name, options) {
|
|
62
37
|
const spinner = options.json ? undefined : ora();
|
|
@@ -68,38 +43,29 @@ export async function newChangeCommand(name, options) {
|
|
|
68
43
|
if (!validation.valid) {
|
|
69
44
|
throw new Error(validation.error);
|
|
70
45
|
}
|
|
71
|
-
|
|
72
|
-
const
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
assertRepoLocalInitiativeLinkPlanningHome(planningHome);
|
|
79
|
-
initiative = await resolveInitiativeLinkReference(options.initiative, {
|
|
80
|
-
store: options.store,
|
|
81
|
-
storePath: options.storePath,
|
|
82
|
-
});
|
|
46
|
+
assertRemovedOptionsAbsent(options);
|
|
47
|
+
const root = await resolveRootForCommand(options, {
|
|
48
|
+
json: options.json,
|
|
49
|
+
failurePayload: { change: null },
|
|
50
|
+
});
|
|
51
|
+
if (!root) {
|
|
52
|
+
return;
|
|
83
53
|
}
|
|
54
|
+
const projectRoot = root.path;
|
|
84
55
|
// Validate schema if provided
|
|
85
56
|
if (options.schema) {
|
|
86
57
|
validateSchemaExists(options.schema, projectRoot);
|
|
87
58
|
}
|
|
88
|
-
const resolvedSchema = options.schema ??
|
|
59
|
+
const resolvedSchema = options.schema ?? root.defaultSchema;
|
|
89
60
|
if (spinner) {
|
|
90
61
|
spinner.start(`Creating change '${name}' with schema '${resolvedSchema}'...`);
|
|
91
62
|
}
|
|
92
|
-
const workspaceGoal = planningHome.kind === 'workspace'
|
|
93
|
-
? options.goal ?? options.description
|
|
94
|
-
: options.goal;
|
|
95
63
|
const result = await createChange(projectRoot, name, {
|
|
96
64
|
schema: options.schema,
|
|
97
|
-
defaultSchema:
|
|
98
|
-
changesDir:
|
|
65
|
+
defaultSchema: root.defaultSchema,
|
|
66
|
+
changesDir: root.changesDir,
|
|
99
67
|
metadata: {
|
|
100
|
-
...(
|
|
101
|
-
...(affectedAreas.length > 0 ? { affected_areas: affectedAreas } : {}),
|
|
102
|
-
...(initiative ? { initiative } : {}),
|
|
68
|
+
...(options.goal ? { goal: options.goal } : {}),
|
|
103
69
|
},
|
|
104
70
|
});
|
|
105
71
|
// If description provided, create README.md with description
|
|
@@ -108,22 +74,21 @@ export async function newChangeCommand(name, options) {
|
|
|
108
74
|
const readmePath = path.join(result.changeDir, 'README.md');
|
|
109
75
|
await fs.writeFile(readmePath, `# ${name}\n\n${options.description}\n`, 'utf-8');
|
|
110
76
|
}
|
|
111
|
-
const payload =
|
|
77
|
+
const payload = {
|
|
78
|
+
change: {
|
|
79
|
+
id: name,
|
|
80
|
+
path: result.changeDir,
|
|
81
|
+
metadataPath: path.join(result.changeDir, '.openspec.yaml'),
|
|
82
|
+
schema: result.schema,
|
|
83
|
+
},
|
|
84
|
+
root: toRootOutput(root),
|
|
85
|
+
};
|
|
112
86
|
if (options.json) {
|
|
113
87
|
printJson(payload);
|
|
114
88
|
return;
|
|
115
89
|
}
|
|
116
90
|
spinner?.stop();
|
|
117
|
-
printCreatedChangeHuman(payload,
|
|
118
|
-
if (planningHome.kind === 'workspace' && !initiative) {
|
|
119
|
-
if (affectedAreas.length > 0) {
|
|
120
|
-
console.log(`Affected areas: ${affectedAreas.join(', ')}`);
|
|
121
|
-
}
|
|
122
|
-
else {
|
|
123
|
-
console.log('Affected areas: unresolved; identify them in change metadata or coordination tasks as planning continues.');
|
|
124
|
-
}
|
|
125
|
-
console.log('Next: run openspec status --change "' + name + '" to inspect workspace planning artifacts.');
|
|
126
|
-
}
|
|
91
|
+
printCreatedChangeHuman(payload, root);
|
|
127
92
|
}
|
|
128
93
|
catch (error) {
|
|
129
94
|
spinner?.stop();
|
|
@@ -4,7 +4,14 @@
|
|
|
4
4
|
* This module contains types, constants, and validation helpers used across
|
|
5
5
|
* multiple artifact workflow commands.
|
|
6
6
|
*/
|
|
7
|
-
import type {
|
|
7
|
+
import type { ReferenceIndexEntry } from '../../core/references.js';
|
|
8
|
+
export interface ChangeCommandStatus {
|
|
9
|
+
severity: 'error' | 'warning';
|
|
10
|
+
code: string;
|
|
11
|
+
message: string;
|
|
12
|
+
target?: string;
|
|
13
|
+
fix?: string;
|
|
14
|
+
}
|
|
8
15
|
export interface TaskItem {
|
|
9
16
|
id: string;
|
|
10
17
|
description: string;
|
|
@@ -14,7 +21,6 @@ export interface ApplyInstructions {
|
|
|
14
21
|
changeName: string;
|
|
15
22
|
changeDir: string;
|
|
16
23
|
schemaName: string;
|
|
17
|
-
initiative?: InitiativeLink;
|
|
18
24
|
contextFiles: Record<string, string[]>;
|
|
19
25
|
progress: {
|
|
20
26
|
total: number;
|
|
@@ -25,8 +31,12 @@ export interface ApplyInstructions {
|
|
|
25
31
|
state: 'blocked' | 'all_done' | 'ready';
|
|
26
32
|
missingArtifacts?: string[];
|
|
27
33
|
instruction: string;
|
|
34
|
+
/** Referenced-store index (read-only upstream context; omitted when none declared) */
|
|
35
|
+
references?: ReferenceIndexEntry[];
|
|
28
36
|
}
|
|
29
37
|
export declare const DEFAULT_SCHEMA = "spec-driven";
|
|
38
|
+
export declare function printJson(payload: unknown): void;
|
|
39
|
+
export declare function statusFromError(error: unknown): ChangeCommandStatus;
|
|
30
40
|
/**
|
|
31
41
|
* Checks if color output is disabled via NO_COLOR env or --no-color flag.
|
|
32
42
|
*/
|
|
@@ -48,7 +58,9 @@ export declare function getAvailableChanges(projectRoot: string, changesDir?: st
|
|
|
48
58
|
* Validates that a change exists and returns available changes if not.
|
|
49
59
|
* Checks directory existence directly to support scaffolded changes (without proposal.md).
|
|
50
60
|
*/
|
|
51
|
-
export declare function validateChangeExists(changeName: string | undefined, projectRoot: string, changesDir?: string
|
|
61
|
+
export declare function validateChangeExists(changeName: string | undefined, projectRoot: string, changesDir?: string, hints?: {
|
|
62
|
+
newChangeHint?: string;
|
|
63
|
+
}): Promise<string>;
|
|
52
64
|
/**
|
|
53
65
|
* Validates that a schema exists and returns available schemas if not.
|
|
54
66
|
*
|
|
@@ -8,6 +8,7 @@ import chalk from 'chalk';
|
|
|
8
8
|
import path from 'path';
|
|
9
9
|
import * as fs from 'fs';
|
|
10
10
|
import { getSchemaDir, listSchemas } from '../../core/artifact-graph/index.js';
|
|
11
|
+
import { isRootSelectionError } from '../../core/root-selection.js';
|
|
11
12
|
import { validateChangeName } from '../../utils/change-utils.js';
|
|
12
13
|
// -----------------------------------------------------------------------------
|
|
13
14
|
// Constants
|
|
@@ -16,6 +17,19 @@ export const DEFAULT_SCHEMA = 'spec-driven';
|
|
|
16
17
|
// -----------------------------------------------------------------------------
|
|
17
18
|
// Utility Functions
|
|
18
19
|
// -----------------------------------------------------------------------------
|
|
20
|
+
export function printJson(payload) {
|
|
21
|
+
console.log(JSON.stringify(payload, null, 2));
|
|
22
|
+
}
|
|
23
|
+
export function statusFromError(error) {
|
|
24
|
+
if (isRootSelectionError(error)) {
|
|
25
|
+
return { ...error.diagnostic };
|
|
26
|
+
}
|
|
27
|
+
return {
|
|
28
|
+
severity: 'error',
|
|
29
|
+
code: 'change_error',
|
|
30
|
+
message: error instanceof Error ? error.message : String(error),
|
|
31
|
+
};
|
|
32
|
+
}
|
|
19
33
|
/**
|
|
20
34
|
* Checks if color output is disabled via NO_COLOR env or --no-color flag.
|
|
21
35
|
*/
|
|
@@ -74,11 +88,14 @@ export async function getAvailableChanges(projectRoot, changesDir = path.join(pr
|
|
|
74
88
|
* Validates that a change exists and returns available changes if not.
|
|
75
89
|
* Checks directory existence directly to support scaffolded changes (without proposal.md).
|
|
76
90
|
*/
|
|
77
|
-
export async function validateChangeExists(changeName, projectRoot, changesDir = path.join(projectRoot, 'openspec', 'changes')) {
|
|
91
|
+
export async function validateChangeExists(changeName, projectRoot, changesDir = path.join(projectRoot, 'openspec', 'changes'), hints = {}) {
|
|
92
|
+
// Hints must stay pasteable: callers with a selected store pass a
|
|
93
|
+
// store-carrying hint so following it lands in the same root.
|
|
94
|
+
const newChangeHint = hints.newChangeHint ?? 'openspec new change <name>';
|
|
78
95
|
if (!changeName) {
|
|
79
96
|
const available = await getAvailableChanges(projectRoot, changesDir);
|
|
80
97
|
if (available.length === 0) {
|
|
81
|
-
throw new Error(
|
|
98
|
+
throw new Error(`No changes found. Create one with: ${newChangeHint}`);
|
|
82
99
|
}
|
|
83
100
|
throw new Error(`Missing required option --change. Available changes:\n ${available.join('\n ')}`);
|
|
84
101
|
}
|
|
@@ -93,7 +110,7 @@ export async function validateChangeExists(changeName, projectRoot, changesDir =
|
|
|
93
110
|
if (!exists) {
|
|
94
111
|
const available = await getAvailableChanges(projectRoot, changesDir);
|
|
95
112
|
if (available.length === 0) {
|
|
96
|
-
throw new Error(`Change '${changeName}' not found. No changes exist. Create one with:
|
|
113
|
+
throw new Error(`Change '${changeName}' not found. No changes exist. Create one with: ${newChangeHint}`);
|
|
97
114
|
}
|
|
98
115
|
throw new Error(`Change '${changeName}' not found. Available changes:\n ${available.join('\n ')}`);
|
|
99
116
|
}
|
|
@@ -7,6 +7,8 @@ import { type ChangeStatus } from '../../core/artifact-graph/index.js';
|
|
|
7
7
|
export interface StatusOptions {
|
|
8
8
|
change?: string;
|
|
9
9
|
schema?: string;
|
|
10
|
+
store?: string;
|
|
11
|
+
storePath?: string;
|
|
10
12
|
json?: boolean;
|
|
11
13
|
}
|
|
12
14
|
export declare function statusCommand(options: StatusOptions): Promise<void>;
|
|
@@ -5,35 +5,44 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import ora from 'ora';
|
|
7
7
|
import chalk from 'chalk';
|
|
8
|
-
import {
|
|
8
|
+
import { getChangeDir } from '../../core/planning-home.js';
|
|
9
|
+
import { resolveRootForCommand, toPlanningHome, toRootOutput, withStoreFlag, isStoreSelectedRoot, } from '../../core/root-selection.js';
|
|
9
10
|
import { loadChangeContext, formatChangeStatus, } from '../../core/artifact-graph/index.js';
|
|
10
11
|
import { validateChangeExists, validateSchemaExists, getAvailableChanges, getStatusIndicator, getStatusColor, } from './shared.js';
|
|
11
12
|
// -----------------------------------------------------------------------------
|
|
12
13
|
// Command Implementation
|
|
13
14
|
// -----------------------------------------------------------------------------
|
|
14
15
|
export async function statusCommand(options) {
|
|
16
|
+
// The root resolves (and the store banner prints) before the spinner starts
|
|
17
|
+
// so the two do not fight over stderr.
|
|
18
|
+
const root = await resolveRootForCommand(options, { json: options.json });
|
|
19
|
+
if (!root) {
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
15
22
|
const spinner = options.json ? undefined : ora('Loading change status...').start();
|
|
16
23
|
try {
|
|
17
|
-
const planningHome =
|
|
18
|
-
const projectRoot =
|
|
24
|
+
const planningHome = toPlanningHome(root);
|
|
25
|
+
const projectRoot = root.path;
|
|
26
|
+
const rootOutput = toRootOutput(root);
|
|
27
|
+
const newChangeHint = withStoreFlag(root, 'openspec new change <name>');
|
|
19
28
|
// Handle no-changes case gracefully — status is informational,
|
|
20
29
|
// so "no changes" is a valid state, not an error.
|
|
21
30
|
if (!options.change) {
|
|
22
|
-
const available = await getAvailableChanges(projectRoot,
|
|
31
|
+
const available = await getAvailableChanges(projectRoot, root.changesDir);
|
|
23
32
|
if (available.length === 0) {
|
|
24
33
|
spinner?.stop();
|
|
25
34
|
if (options.json) {
|
|
26
|
-
console.log(JSON.stringify({ changes: [], message: 'No active changes.' }, null, 2));
|
|
35
|
+
console.log(JSON.stringify({ changes: [], message: 'No active changes.', root: rootOutput }, null, 2));
|
|
27
36
|
return;
|
|
28
37
|
}
|
|
29
|
-
console.log(
|
|
38
|
+
console.log(`No active changes. Create one with: ${newChangeHint}`);
|
|
30
39
|
return;
|
|
31
40
|
}
|
|
32
41
|
// Changes exist but --change not provided
|
|
33
42
|
spinner?.stop();
|
|
34
43
|
throw new Error(`Missing required option --change. Available changes:\n ${available.join('\n ')}`);
|
|
35
44
|
}
|
|
36
|
-
const changeName = await validateChangeExists(options.change, projectRoot,
|
|
45
|
+
const changeName = await validateChangeExists(options.change, projectRoot, root.changesDir, { newChangeHint });
|
|
37
46
|
// Validate schema if explicitly provided
|
|
38
47
|
if (options.schema) {
|
|
39
48
|
validateSchemaExists(options.schema, projectRoot);
|
|
@@ -43,10 +52,10 @@ export async function statusCommand(options) {
|
|
|
43
52
|
changeDir: getChangeDir(planningHome, changeName),
|
|
44
53
|
planningHome,
|
|
45
54
|
});
|
|
46
|
-
const status = formatChangeStatus(context);
|
|
55
|
+
const status = formatChangeStatus(context, isStoreSelectedRoot(root) ? { storeId: root.storeId } : {});
|
|
47
56
|
spinner?.stop();
|
|
48
57
|
if (options.json) {
|
|
49
|
-
console.log(JSON.stringify(status, null, 2));
|
|
58
|
+
console.log(JSON.stringify({ ...status, root: rootOutput }, null, 2));
|
|
50
59
|
return;
|
|
51
60
|
}
|
|
52
61
|
printStatusText(status);
|
|
@@ -61,14 +70,7 @@ export function printStatusText(status) {
|
|
|
61
70
|
const total = status.artifacts.length;
|
|
62
71
|
console.log(`Change: ${status.changeName}`);
|
|
63
72
|
console.log(`Schema: ${status.schemaName}`);
|
|
64
|
-
if (status.
|
|
65
|
-
console.log(`Initiative: ${status.initiative.store}/${status.initiative.id}`);
|
|
66
|
-
}
|
|
67
|
-
if (status.planningHome) {
|
|
68
|
-
const label = status.planningHome.kind === 'workspace'
|
|
69
|
-
? `workspace${status.planningHome.workspaceName ? ` (${status.planningHome.workspaceName})` : ''}`
|
|
70
|
-
: 'repo';
|
|
71
|
-
console.log(`Planning home: ${label}`);
|
|
73
|
+
if (status.changeRoot) {
|
|
72
74
|
console.log(`Change root: ${status.changeRoot}`);
|
|
73
75
|
}
|
|
74
76
|
console.log(`Progress: ${doneCount}/${total} artifacts complete`);
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { type OpenerDefinition, type OpenerScanOptions } from '../core/openers.js';
|
|
2
|
+
import { StoreError } from '../core/store/errors.js';
|
|
3
|
+
import { type Workset, type WorksetMember } from '../core/worksets.js';
|
|
4
|
+
/** Concurrent stats; the first invalid flag (by flag order) reports. */
|
|
5
|
+
export declare function resolveMemberFlags(flags: string[]): Promise<WorksetMember[]>;
|
|
6
|
+
/** One spelling of "this tool id must exist in the merged table". */
|
|
7
|
+
export declare function assertKnownTool(tool: string, table: OpenerDefinition[]): void;
|
|
8
|
+
/** Final assembly shared by both compose paths: one validation rule. */
|
|
9
|
+
export declare function finalizeWorkset(name: string, members: WorksetMember[], tool: string | undefined, table: OpenerDefinition[]): Workset;
|
|
10
|
+
/** The aligned `<name> <path>` rows used by list, remove, and the
|
|
11
|
+
* open fallback; callers pick the stream and indent. */
|
|
12
|
+
export declare function formatMemberRows(members: WorksetMember[]): string[];
|
|
13
|
+
export declare function toolUnknownError(toolId: string, table: OpenerDefinition[]): StoreError;
|
|
14
|
+
/** Stops at the first installed alternative instead of scanning all. */
|
|
15
|
+
export declare function firstInstalledAlternative(table: OpenerDefinition[], excludeId: string | undefined, scan?: OpenerScanOptions): string | null;
|
|
16
|
+
export declare function toolUnavailableError(opener: OpenerDefinition, table: OpenerDefinition[], worksetName: string, scan?: OpenerScanOptions): StoreError;
|
|
17
|
+
/** Interactive open with no saved tool and nothing installed at all. */
|
|
18
|
+
export declare function noToolInstalledError(table: OpenerDefinition[], worksetName: string): StoreError;
|
|
19
|
+
//# sourceMappingURL=workset-input.d.ts.map
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Input resolution and error builders shared by the workset command
|
|
3
|
+
* and its interactive prompt flows.
|
|
4
|
+
*/
|
|
5
|
+
import * as path from 'node:path';
|
|
6
|
+
import { pathIsDirectory } from '../core/file-state.js';
|
|
7
|
+
import { findOpener, isOpenerCommandAvailable, isOpenerEnabled, } from '../core/openers.js';
|
|
8
|
+
import { StoreError } from '../core/store/errors.js';
|
|
9
|
+
import { expandUserPath } from '../core/store/operations.js';
|
|
10
|
+
import { getGlobalConfigPath } from '../core/global-config.js';
|
|
11
|
+
import { memberLabelProblem, memberListProblem, } from '../core/worksets.js';
|
|
12
|
+
function memberInvalidError(problem) {
|
|
13
|
+
return new StoreError(`Invalid workset member: ${problem}.`, 'workset_member_invalid', {
|
|
14
|
+
target: 'workset.member',
|
|
15
|
+
fix: 'Pass --member <path> with an existing folder, or --member <name>=<path> to label it.',
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
/** `--member <path>` or `--member <name>=<path>` (the first `=` splits). */
|
|
19
|
+
async function resolveMemberFlag(raw) {
|
|
20
|
+
const separator = raw.indexOf('=');
|
|
21
|
+
const label = separator > 0 ? raw.slice(0, separator) : undefined;
|
|
22
|
+
const rawPath = separator > 0 ? raw.slice(separator + 1) : raw;
|
|
23
|
+
if (rawPath.length === 0) {
|
|
24
|
+
throw memberInvalidError(`'${raw}' has no path`);
|
|
25
|
+
}
|
|
26
|
+
const resolvedPath = path.resolve(expandUserPath(rawPath));
|
|
27
|
+
if (!(await pathIsDirectory(resolvedPath))) {
|
|
28
|
+
throw memberInvalidError(`'${rawPath}' is not an existing folder`);
|
|
29
|
+
}
|
|
30
|
+
const name = label ?? path.basename(resolvedPath);
|
|
31
|
+
const labelProblem = memberLabelProblem(name);
|
|
32
|
+
if (labelProblem !== null) {
|
|
33
|
+
throw memberInvalidError(labelProblem);
|
|
34
|
+
}
|
|
35
|
+
return { name, path: resolvedPath };
|
|
36
|
+
}
|
|
37
|
+
/** Concurrent stats; the first invalid flag (by flag order) reports. */
|
|
38
|
+
export async function resolveMemberFlags(flags) {
|
|
39
|
+
const settled = await Promise.allSettled(flags.map(resolveMemberFlag));
|
|
40
|
+
const members = [];
|
|
41
|
+
for (const result of settled) {
|
|
42
|
+
if (result.status === 'rejected') {
|
|
43
|
+
throw result.reason;
|
|
44
|
+
}
|
|
45
|
+
members.push(result.value);
|
|
46
|
+
}
|
|
47
|
+
return members;
|
|
48
|
+
}
|
|
49
|
+
/** One spelling of "this tool id must exist in the merged table". */
|
|
50
|
+
export function assertKnownTool(tool, table) {
|
|
51
|
+
if (findOpener(table, tool) === null) {
|
|
52
|
+
throw toolUnknownError(tool, table);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
/** Final assembly shared by both compose paths: one validation rule. */
|
|
56
|
+
export function finalizeWorkset(name, members, tool, table) {
|
|
57
|
+
const problem = memberListProblem(members);
|
|
58
|
+
if (problem !== null) {
|
|
59
|
+
throw memberInvalidError(problem);
|
|
60
|
+
}
|
|
61
|
+
if (tool !== undefined) {
|
|
62
|
+
assertKnownTool(tool, table);
|
|
63
|
+
}
|
|
64
|
+
return {
|
|
65
|
+
name,
|
|
66
|
+
...(tool !== undefined ? { tool } : {}),
|
|
67
|
+
members,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
/** The aligned `<name> <path>` rows used by list, remove, and the
|
|
71
|
+
* open fallback; callers pick the stream and indent. */
|
|
72
|
+
export function formatMemberRows(members) {
|
|
73
|
+
const width = Math.max(...members.map((member) => member.name.length));
|
|
74
|
+
return members.map((member) => `${member.name.padEnd(width)} ${member.path}`);
|
|
75
|
+
}
|
|
76
|
+
export function toolUnknownError(toolId, table) {
|
|
77
|
+
const knownIds = table
|
|
78
|
+
.filter((opener) => isOpenerEnabled(opener))
|
|
79
|
+
.map((opener) => opener.id)
|
|
80
|
+
.join(', ');
|
|
81
|
+
return new StoreError(`Unknown tool '${toolId}'.`, 'workset_tool_unknown', {
|
|
82
|
+
target: 'workset.tool',
|
|
83
|
+
fix: `Known tools: ${knownIds}. Add new tools under "openers" in ${getGlobalConfigPath()}.`,
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
/** Stops at the first installed alternative instead of scanning all. */
|
|
87
|
+
export function firstInstalledAlternative(table, excludeId, scan) {
|
|
88
|
+
return (table.find((candidate) => candidate.id !== excludeId &&
|
|
89
|
+
isOpenerEnabled(candidate) &&
|
|
90
|
+
isOpenerCommandAvailable(candidate.command, scan))?.id ?? null);
|
|
91
|
+
}
|
|
92
|
+
export function toolUnavailableError(opener, table, worksetName, scan) {
|
|
93
|
+
const alternative = firstInstalledAlternative(table, opener.id, scan);
|
|
94
|
+
return new StoreError(`${opener.label} ('${opener.command}') is not on PATH.`, 'workset_tool_unavailable', {
|
|
95
|
+
target: 'workset.tool',
|
|
96
|
+
fix: alternative !== null
|
|
97
|
+
? `Install '${opener.command}' or run: openspec workset open ${worksetName} --tool ${alternative}`
|
|
98
|
+
: `Install '${opener.command}', then rerun: openspec workset open ${worksetName}`,
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
/** Interactive open with no saved tool and nothing installed at all. */
|
|
102
|
+
export function noToolInstalledError(table, worksetName) {
|
|
103
|
+
const commands = table
|
|
104
|
+
.filter((opener) => isOpenerEnabled(opener))
|
|
105
|
+
.map((opener) => opener.command)
|
|
106
|
+
.join(', ');
|
|
107
|
+
return new StoreError('None of the known tools is on PATH.', 'workset_tool_unavailable', {
|
|
108
|
+
target: 'workset.tool',
|
|
109
|
+
fix: `Install one of: ${commands}. Then rerun: openspec workset open ${worksetName}`,
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
//# sourceMappingURL=workset-input.js.map
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { type OpenerChoice, type OpenerDefinition } from '../core/openers.js';
|
|
2
|
+
import { type Workset } from '../core/worksets.js';
|
|
3
|
+
export interface ComposeInput {
|
|
4
|
+
memberFlags: string[];
|
|
5
|
+
tool?: string;
|
|
6
|
+
}
|
|
7
|
+
export declare function composeInteractively(givenName: string | undefined, input: ComposeInput, table: OpenerDefinition[]): Promise<Workset>;
|
|
8
|
+
export declare function promptToolFromChoices(available: OpenerChoice[]): Promise<string>;
|
|
9
|
+
export declare function promptOpenNow(label: string): Promise<boolean>;
|
|
10
|
+
/** Prints the workset (decision 13: remove shows what it removes). */
|
|
11
|
+
export declare function confirmRemoveInteractively(workset: Workset): Promise<boolean>;
|
|
12
|
+
//# sourceMappingURL=workset-prompts.d.ts.map
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The workset command's interactive prompt flows (the compose wizard,
|
|
3
|
+
* the open-time tool select, the remove confirm). @inquirer is always
|
|
4
|
+
* imported dynamically at the call site - never at module top.
|
|
5
|
+
*/
|
|
6
|
+
import * as path from 'node:path';
|
|
7
|
+
import { pathIsDirectory } from '../core/file-state.js';
|
|
8
|
+
import { listOpenerChoices, } from '../core/openers.js';
|
|
9
|
+
import { expandUserPath } from '../core/store/operations.js';
|
|
10
|
+
import { memberLabelProblem, validateWorksetName, } from '../core/worksets.js';
|
|
11
|
+
import { asErrorMessage } from './shared-output.js';
|
|
12
|
+
import { assertKnownTool, finalizeWorkset, formatMemberRows, resolveMemberFlags, } from './workset-input.js';
|
|
13
|
+
export async function composeInteractively(givenName, input, table) {
|
|
14
|
+
const prompts = await import('@inquirer/prompts');
|
|
15
|
+
console.log('[1/3] Name the workset');
|
|
16
|
+
let name;
|
|
17
|
+
if (givenName !== undefined) {
|
|
18
|
+
name = validateWorksetName(givenName);
|
|
19
|
+
console.log(` Workset name: ${name}`);
|
|
20
|
+
}
|
|
21
|
+
else {
|
|
22
|
+
name = await prompts.input({
|
|
23
|
+
message: 'Workset name:',
|
|
24
|
+
required: true,
|
|
25
|
+
validate(value) {
|
|
26
|
+
try {
|
|
27
|
+
validateWorksetName(value);
|
|
28
|
+
return true;
|
|
29
|
+
}
|
|
30
|
+
catch (error) {
|
|
31
|
+
return asErrorMessage(error);
|
|
32
|
+
}
|
|
33
|
+
},
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
// Flag-provided pieces are validated before any prompting, so a
|
|
37
|
+
// bad flag or tool cannot discard a finished wizard walk.
|
|
38
|
+
if (input.tool !== undefined) {
|
|
39
|
+
assertKnownTool(input.tool, table);
|
|
40
|
+
}
|
|
41
|
+
console.log('');
|
|
42
|
+
console.log('[2/3] Add member folders (the first one is the primary - sessions start there)');
|
|
43
|
+
const members = await resolveMemberFlags(input.memberFlags);
|
|
44
|
+
if (members.length > 0) {
|
|
45
|
+
finalizeWorkset(name, members, input.tool, table);
|
|
46
|
+
for (const member of members) {
|
|
47
|
+
console.log(` Added '${member.name}' (${member.path})`);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
while (true) {
|
|
51
|
+
if (members.length > 0) {
|
|
52
|
+
const next = await prompts.select({
|
|
53
|
+
message: 'Add another folder or finish:',
|
|
54
|
+
choices: [
|
|
55
|
+
{ name: 'Finish', value: 'finish' },
|
|
56
|
+
{ name: 'Add another folder', value: 'add' },
|
|
57
|
+
],
|
|
58
|
+
default: 'finish',
|
|
59
|
+
});
|
|
60
|
+
if (next === 'finish') {
|
|
61
|
+
break;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
const rawPath = await prompts.input({
|
|
65
|
+
message: 'Folder path:',
|
|
66
|
+
...(members.length === 0 ? { default: '.', prefill: 'editable' } : {}),
|
|
67
|
+
required: true,
|
|
68
|
+
async validate(value) {
|
|
69
|
+
const resolved = path.resolve(expandUserPath(value));
|
|
70
|
+
if (!(await pathIsDirectory(resolved))) {
|
|
71
|
+
return `'${value}' is not an existing folder`;
|
|
72
|
+
}
|
|
73
|
+
return true;
|
|
74
|
+
},
|
|
75
|
+
});
|
|
76
|
+
const resolvedPath = path.resolve(expandUserPath(rawPath));
|
|
77
|
+
let label = path.basename(resolvedPath);
|
|
78
|
+
const collision = members.some((member) => member.name === label);
|
|
79
|
+
if (memberLabelProblem(label) !== null || collision) {
|
|
80
|
+
label = await prompts.input({
|
|
81
|
+
message: 'Name this member (the folder label):',
|
|
82
|
+
required: true,
|
|
83
|
+
validate(value) {
|
|
84
|
+
const problem = memberLabelProblem(value);
|
|
85
|
+
if (problem !== null) {
|
|
86
|
+
return problem;
|
|
87
|
+
}
|
|
88
|
+
if (members.some((member) => member.name === value)) {
|
|
89
|
+
return `duplicate member name '${value}'`;
|
|
90
|
+
}
|
|
91
|
+
return true;
|
|
92
|
+
},
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
members.push({ name: label, path: resolvedPath });
|
|
96
|
+
console.log(` Added '${label}' (${resolvedPath})`);
|
|
97
|
+
}
|
|
98
|
+
console.log('');
|
|
99
|
+
console.log('[3/3] Choose your tool');
|
|
100
|
+
let tool = input.tool;
|
|
101
|
+
if (tool === undefined) {
|
|
102
|
+
const choices = listOpenerChoices(table);
|
|
103
|
+
const available = choices.filter((choice) => choice.available);
|
|
104
|
+
if (available.length === 0) {
|
|
105
|
+
console.log(' None of the known tools is on PATH; not saving a preference.');
|
|
106
|
+
console.log(` (Known tools: ${choices.map((choice) => `${choice.opener.id} ${choice.note ?? ''}`.trim()).join(', ')})`);
|
|
107
|
+
}
|
|
108
|
+
else {
|
|
109
|
+
tool = await promptToolFromChoices(available);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
return finalizeWorkset(name, members, tool, table);
|
|
113
|
+
}
|
|
114
|
+
export async function promptToolFromChoices(available) {
|
|
115
|
+
const { select } = await import('@inquirer/prompts');
|
|
116
|
+
return select({
|
|
117
|
+
message: 'Open with:',
|
|
118
|
+
choices: available.map((choice) => ({
|
|
119
|
+
name: choice.opener.label,
|
|
120
|
+
value: choice.opener.id,
|
|
121
|
+
})),
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
export async function promptOpenNow(label) {
|
|
125
|
+
const { confirm } = await import('@inquirer/prompts');
|
|
126
|
+
return confirm({
|
|
127
|
+
message: `Open it now in ${label}?`,
|
|
128
|
+
default: true,
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
/** Prints the workset (decision 13: remove shows what it removes). */
|
|
132
|
+
export async function confirmRemoveInteractively(workset) {
|
|
133
|
+
const { confirm } = await import('@inquirer/prompts');
|
|
134
|
+
console.log(`Workset '${workset.name}':`);
|
|
135
|
+
for (const row of formatMemberRows(workset.members)) {
|
|
136
|
+
console.log(` ${row}`);
|
|
137
|
+
}
|
|
138
|
+
return confirm({
|
|
139
|
+
message: `Remove workset '${workset.name}'? (member folders are never touched)`,
|
|
140
|
+
default: false,
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
//# sourceMappingURL=workset-prompts.js.map
|