@gordon.gan/specflow 1.1.1 → 1.2.0-beta
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 +6 -0
- package/dist/cli/commands/change-archive.js +4 -4
- package/dist/cli/commands/change-new.js +5 -5
- package/dist/cli/commands/change-phase.js +4 -4
- package/dist/cli/commands/change-status.js +4 -4
- package/dist/cli/commands/context.d.ts +18 -0
- package/dist/cli/commands/context.js +125 -0
- package/dist/cli/commands/instructions.d.ts +4 -1
- package/dist/cli/commands/instructions.js +58 -9
- package/dist/cli/commands/show.d.ts +22 -0
- package/dist/cli/commands/show.js +92 -0
- package/dist/cli/commands/store.d.ts +16 -0
- package/dist/cli/commands/store.js +221 -0
- package/dist/cli/commands/validate.d.ts +16 -0
- package/dist/cli/commands/validate.js +34 -4
- package/dist/cli/commands/workset.d.ts +12 -0
- package/dist/cli/commands/workset.js +235 -0
- package/dist/cli/index.js +8 -0
- package/dist/cli/shared/store-option.d.ts +11 -0
- package/dist/cli/shared/store-option.js +40 -0
- package/dist/core/artifact-graph/instruction-loader.d.ts +17 -0
- package/dist/core/artifact-graph/instruction-loader.js +2 -0
- package/dist/core/artifact-graph/types.d.ts +2 -2
- package/dist/core/context-assembly.d.ts +9 -0
- package/dist/core/context-assembly.js +68 -0
- package/dist/core/diagnostics.d.ts +11 -0
- package/dist/core/diagnostics.js +18 -0
- package/dist/core/file-state.d.ts +23 -0
- package/dist/core/file-state.js +101 -0
- package/dist/core/global-config.d.ts +26 -0
- package/dist/core/global-config.js +77 -0
- package/dist/core/opener-launch.d.ts +3 -0
- package/dist/core/opener-launch.js +20 -0
- package/dist/core/openers.d.ts +23 -0
- package/dist/core/openers.js +20 -0
- package/dist/core/project-config.d.ts +14 -0
- package/dist/core/project-config.js +73 -0
- package/dist/core/reference-index.d.ts +8 -0
- package/dist/core/reference-index.js +80 -0
- package/dist/core/references.d.ts +17 -0
- package/dist/core/references.js +51 -0
- package/dist/core/relationship-health.d.ts +22 -0
- package/dist/core/relationship-health.js +68 -0
- package/dist/core/root-selection.d.ts +26 -0
- package/dist/core/root-selection.js +197 -0
- package/dist/core/store/errors.d.ts +2 -0
- package/dist/core/store/errors.js +1 -0
- package/dist/core/store/foundation.d.ts +141 -0
- package/dist/core/store/foundation.js +79 -0
- package/dist/core/store/health.d.ts +13 -0
- package/dist/core/store/health.js +117 -0
- package/dist/core/store/operations.d.ts +56 -0
- package/dist/core/store/operations.js +268 -0
- package/dist/core/store/registry.d.ts +15 -0
- package/dist/core/store/registry.js +128 -0
- package/dist/core/working-set.d.ts +30 -0
- package/dist/core/working-set.js +26 -0
- package/dist/core/worksets.d.ts +71 -0
- package/dist/core/worksets.js +134 -0
- package/dist/integrations/shared/skill-renderer.d.ts +6 -0
- package/dist/integrations/shared/skill-renderer.js +22 -0
- package/package.json +1 -1
- package/skills/specflow-apply/SKILL.md +5 -27
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
export const REFERENCE_INDEX_BUDGET_BYTES = 50 * 1024;
|
|
2
|
+
export function normalizeReferences(references, rootStoreId) {
|
|
3
|
+
const seen = new Set();
|
|
4
|
+
const output = [];
|
|
5
|
+
for (const ref of references) {
|
|
6
|
+
if (rootStoreId && ref.id === rootStoreId) {
|
|
7
|
+
continue;
|
|
8
|
+
}
|
|
9
|
+
if (seen.has(ref.id)) {
|
|
10
|
+
continue;
|
|
11
|
+
}
|
|
12
|
+
seen.add(ref.id);
|
|
13
|
+
output.push(ref);
|
|
14
|
+
}
|
|
15
|
+
return output;
|
|
16
|
+
}
|
|
17
|
+
export function sanitizeReferenceField(value, maxLength = 200) {
|
|
18
|
+
return value
|
|
19
|
+
.replace(/[\u0000-\u001F\u007F]/g, ' ')
|
|
20
|
+
.replace(/\s+/g, ' ')
|
|
21
|
+
.trim()
|
|
22
|
+
.slice(0, maxLength);
|
|
23
|
+
}
|
|
24
|
+
export function buildReferenceIndex(entries) {
|
|
25
|
+
const lines = [];
|
|
26
|
+
const diagnostics = [];
|
|
27
|
+
let truncated = false;
|
|
28
|
+
for (const entry of entries) {
|
|
29
|
+
const line = `- ${sanitizeReferenceField(entry.storeId)}/${sanitizeReferenceField(entry.specId)}: ${sanitizeReferenceField(entry.purpose)} | ${entry.fetchCommand}`;
|
|
30
|
+
const candidate = [...lines, line].join('\n');
|
|
31
|
+
if (Buffer.byteLength(candidate, 'utf8') > REFERENCE_INDEX_BUDGET_BYTES) {
|
|
32
|
+
truncated = true;
|
|
33
|
+
break;
|
|
34
|
+
}
|
|
35
|
+
lines.push(line);
|
|
36
|
+
}
|
|
37
|
+
if (truncated) {
|
|
38
|
+
diagnostics.push({
|
|
39
|
+
severity: 'warning',
|
|
40
|
+
code: 'reference_index_truncated',
|
|
41
|
+
message: 'Referenced specification index exceeded the 50 KiB budget.',
|
|
42
|
+
target: 'references.index',
|
|
43
|
+
fix: 'Use specflow show or specflow context to fetch remaining referenced specs.',
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
return {
|
|
47
|
+
rendered: lines.join('\n'),
|
|
48
|
+
truncated,
|
|
49
|
+
diagnostics,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { Diagnostic } from './diagnostics.js';
|
|
2
|
+
import type { NormalizedReference } from './project-config.js';
|
|
3
|
+
import type { ParsedProjectConfig } from './project-config.js';
|
|
4
|
+
import type { StoreRegistry } from './store/foundation.js';
|
|
5
|
+
import type { ResolvedPlanningRoot } from './root-selection.js';
|
|
6
|
+
export interface RelationshipHealthResult {
|
|
7
|
+
readonly rootHealth: readonly Diagnostic[];
|
|
8
|
+
readonly selectedStoreHealth: readonly Diagnostic[];
|
|
9
|
+
readonly referenceHealth: Array<{
|
|
10
|
+
storeId: string;
|
|
11
|
+
diagnostics: Diagnostic[];
|
|
12
|
+
}>;
|
|
13
|
+
readonly registryHealth: readonly Diagnostic[];
|
|
14
|
+
readonly pointerHealth: readonly Diagnostic[];
|
|
15
|
+
readonly mutationsPerformed: false;
|
|
16
|
+
}
|
|
17
|
+
export declare function inspectRelationships(input: {
|
|
18
|
+
resolved: ResolvedPlanningRoot;
|
|
19
|
+
references: readonly NormalizedReference[];
|
|
20
|
+
registry: StoreRegistry;
|
|
21
|
+
projectConfig?: ParsedProjectConfig;
|
|
22
|
+
}): Promise<RelationshipHealthResult>;
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { sortDiagnostics } from './diagnostics.js';
|
|
2
|
+
import { inspectStoreEntry } from './store/health.js';
|
|
3
|
+
import { hasPlanningContent } from './root-selection.js';
|
|
4
|
+
export async function inspectRelationships(input) {
|
|
5
|
+
const pointerHealth = [];
|
|
6
|
+
if (input.projectConfig?.store && input.resolved.source === 'nearest') {
|
|
7
|
+
pointerHealth.push({
|
|
8
|
+
severity: 'info',
|
|
9
|
+
code: 'store_pointer_ignored',
|
|
10
|
+
message: `Project store pointer '${input.projectConfig.store}' is ignored because local planning content exists.`,
|
|
11
|
+
target: 'config.store',
|
|
12
|
+
});
|
|
13
|
+
}
|
|
14
|
+
const rootHealth = [...input.resolved.diagnostics];
|
|
15
|
+
if (!(await hasPlanningContent(input.resolved.root)) && !input.resolved.storeId) {
|
|
16
|
+
rootHealth.push({
|
|
17
|
+
severity: 'warning',
|
|
18
|
+
code: 'root_empty_planning',
|
|
19
|
+
message: 'Resolved planning root has no baseline specs or active changes.',
|
|
20
|
+
target: 'planning.root',
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
const selectedStoreHealth = [];
|
|
24
|
+
if (input.resolved.storeId) {
|
|
25
|
+
const entry = input.registry.stores[input.resolved.storeId];
|
|
26
|
+
if (!entry) {
|
|
27
|
+
selectedStoreHealth.push({
|
|
28
|
+
severity: 'error',
|
|
29
|
+
code: 'store_not_registered',
|
|
30
|
+
message: `Selected store '${input.resolved.storeId}' is not registered.`,
|
|
31
|
+
target: `stores.${input.resolved.storeId}`,
|
|
32
|
+
fix: `Run specflow store register --id ${input.resolved.storeId} <path> --yes`,
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
else {
|
|
36
|
+
selectedStoreHealth.push(...(await inspectStoreEntry(input.resolved.storeId, entry)));
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
const referenceHealth = await Promise.all(input.references.map(async (ref) => {
|
|
40
|
+
const entry = input.registry.stores[ref.id];
|
|
41
|
+
if (!entry) {
|
|
42
|
+
return {
|
|
43
|
+
storeId: ref.id,
|
|
44
|
+
diagnostics: [
|
|
45
|
+
{
|
|
46
|
+
severity: 'warning',
|
|
47
|
+
code: 'reference_unregistered',
|
|
48
|
+
message: `Referenced store '${ref.id}' is not registered.`,
|
|
49
|
+
target: `references.${ref.id}`,
|
|
50
|
+
fix: `Run specflow store register --id ${ref.id} <path> --yes`,
|
|
51
|
+
},
|
|
52
|
+
],
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
return {
|
|
56
|
+
storeId: ref.id,
|
|
57
|
+
diagnostics: [...(await inspectStoreEntry(ref.id, entry))],
|
|
58
|
+
};
|
|
59
|
+
}));
|
|
60
|
+
return {
|
|
61
|
+
rootHealth: sortDiagnostics(rootHealth),
|
|
62
|
+
selectedStoreHealth: sortDiagnostics(selectedStoreHealth),
|
|
63
|
+
referenceHealth,
|
|
64
|
+
registryHealth: [],
|
|
65
|
+
pointerHealth: sortDiagnostics(pointerHealth),
|
|
66
|
+
mutationsPerformed: false,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { Diagnostic } from './diagnostics.js';
|
|
2
|
+
import type { GlobalConfig } from './global-config.js';
|
|
3
|
+
import { type ParsedProjectConfig } from './project-config.js';
|
|
4
|
+
import type { StoreRegistry } from './store/foundation.js';
|
|
5
|
+
export type RootSource = 'store' | 'declared' | 'global_default' | 'nearest' | 'implicit';
|
|
6
|
+
export interface ResolvedPlanningRoot {
|
|
7
|
+
readonly root: string;
|
|
8
|
+
readonly specsDir: string;
|
|
9
|
+
readonly changesDir: string;
|
|
10
|
+
readonly archiveDir: string;
|
|
11
|
+
readonly source: RootSource;
|
|
12
|
+
readonly storeId?: string;
|
|
13
|
+
readonly diagnostics: readonly Diagnostic[];
|
|
14
|
+
}
|
|
15
|
+
export interface ResolvePlanningRootOptions {
|
|
16
|
+
readonly startPath?: string;
|
|
17
|
+
readonly store?: string;
|
|
18
|
+
readonly allowImplicitRoot?: boolean;
|
|
19
|
+
readonly registry?: StoreRegistry;
|
|
20
|
+
readonly globalConfig?: GlobalConfig;
|
|
21
|
+
readonly readConfig?: (projectRoot: string) => ParsedProjectConfig;
|
|
22
|
+
}
|
|
23
|
+
export declare function hasBaselineSpec(root: string): Promise<boolean>;
|
|
24
|
+
export declare function hasNonArchiveChange(root: string): Promise<boolean>;
|
|
25
|
+
export declare function hasPlanningContent(root: string): Promise<boolean>;
|
|
26
|
+
export declare function resolvePlanningRoot(options?: ResolvePlanningRootOptions): Promise<ResolvedPlanningRoot>;
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import yaml from 'js-yaml';
|
|
3
|
+
import { promises as fs } from 'node:fs';
|
|
4
|
+
import * as path from 'node:path';
|
|
5
|
+
import { parseProjectConfig } from './project-config.js';
|
|
6
|
+
import { findProjectRoot } from '../utils/project-root.js';
|
|
7
|
+
const ARCHIVE_DIR_NAME = 'archive';
|
|
8
|
+
async function directoryHasEntries(dirPath) {
|
|
9
|
+
try {
|
|
10
|
+
const entries = await fs.readdir(dirPath);
|
|
11
|
+
return entries.length > 0;
|
|
12
|
+
}
|
|
13
|
+
catch {
|
|
14
|
+
return false;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
export async function hasBaselineSpec(root) {
|
|
18
|
+
const specsDir = path.join(root, 'specflow', 'specs');
|
|
19
|
+
if (!(await directoryHasEntries(specsDir))) {
|
|
20
|
+
return false;
|
|
21
|
+
}
|
|
22
|
+
const entries = await fs.readdir(specsDir, { withFileTypes: true });
|
|
23
|
+
for (const entry of entries) {
|
|
24
|
+
if (entry.isDirectory()) {
|
|
25
|
+
const specPath = path.join(specsDir, entry.name, 'spec.md');
|
|
26
|
+
try {
|
|
27
|
+
await fs.stat(specPath);
|
|
28
|
+
return true;
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
// continue
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
export async function hasNonArchiveChange(root) {
|
|
38
|
+
const changesDir = path.join(root, 'specflow', 'changes');
|
|
39
|
+
if (!(await directoryHasEntries(changesDir))) {
|
|
40
|
+
return false;
|
|
41
|
+
}
|
|
42
|
+
const entries = await fs.readdir(changesDir, { withFileTypes: true });
|
|
43
|
+
for (const entry of entries) {
|
|
44
|
+
if (!entry.isDirectory() || entry.name === ARCHIVE_DIR_NAME) {
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
return true;
|
|
48
|
+
}
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
export async function hasPlanningContent(root) {
|
|
52
|
+
return (await hasBaselineSpec(root)) || (await hasNonArchiveChange(root));
|
|
53
|
+
}
|
|
54
|
+
function resolveFromStoreId(storeId, registry, source) {
|
|
55
|
+
const entry = registry.stores[storeId];
|
|
56
|
+
if (!entry) {
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
const root = entry.backend.local_path;
|
|
60
|
+
return {
|
|
61
|
+
root,
|
|
62
|
+
specsDir: path.join(root, 'specflow', 'specs'),
|
|
63
|
+
changesDir: path.join(root, 'specflow', 'changes'),
|
|
64
|
+
archiveDir: path.join(root, 'specflow', 'changes', ARCHIVE_DIR_NAME),
|
|
65
|
+
source,
|
|
66
|
+
storeId,
|
|
67
|
+
diagnostics: [],
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
function buildImplicitFailure(startPath) {
|
|
71
|
+
return {
|
|
72
|
+
root: startPath,
|
|
73
|
+
specsDir: path.join(startPath, 'specflow', 'specs'),
|
|
74
|
+
changesDir: path.join(startPath, 'specflow', 'changes'),
|
|
75
|
+
archiveDir: path.join(startPath, 'specflow', 'changes', ARCHIVE_DIR_NAME),
|
|
76
|
+
source: 'implicit',
|
|
77
|
+
diagnostics: [
|
|
78
|
+
{
|
|
79
|
+
severity: 'error',
|
|
80
|
+
code: 'planning_root_not_found',
|
|
81
|
+
message: 'No planning root could be resolved.',
|
|
82
|
+
target: 'planning.root',
|
|
83
|
+
fix: 'Run specflow init or register a planning store.',
|
|
84
|
+
},
|
|
85
|
+
],
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
export async function resolvePlanningRoot(options = {}) {
|
|
89
|
+
const startPath = path.resolve(options.startPath ?? process.cwd());
|
|
90
|
+
const registry = options.registry ?? { version: 1, stores: {} };
|
|
91
|
+
const globalConfig = options.globalConfig ?? {};
|
|
92
|
+
const readConfig = options.readConfig ??
|
|
93
|
+
((projectRoot) => {
|
|
94
|
+
const configPath = path.join(projectRoot, 'specflow', 'config.yaml');
|
|
95
|
+
try {
|
|
96
|
+
const content = readFileSync(configPath, 'utf-8');
|
|
97
|
+
return parseProjectConfig(yaml.load(content));
|
|
98
|
+
}
|
|
99
|
+
catch {
|
|
100
|
+
return parseProjectConfig({});
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
if (options.store) {
|
|
104
|
+
const explicit = resolveFromStoreId(options.store, registry, 'store');
|
|
105
|
+
if (explicit) {
|
|
106
|
+
return explicit;
|
|
107
|
+
}
|
|
108
|
+
return {
|
|
109
|
+
...buildImplicitFailure(startPath),
|
|
110
|
+
source: 'store',
|
|
111
|
+
storeId: options.store,
|
|
112
|
+
diagnostics: [
|
|
113
|
+
{
|
|
114
|
+
severity: 'error',
|
|
115
|
+
code: 'store_not_registered',
|
|
116
|
+
message: `Store '${options.store}' is not registered.`,
|
|
117
|
+
target: `stores.${options.store}`,
|
|
118
|
+
fix: 'Run specflow store register to add the checkout.',
|
|
119
|
+
},
|
|
120
|
+
],
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
const nearest = findProjectRoot(startPath);
|
|
124
|
+
if (nearest) {
|
|
125
|
+
const config = readConfig(nearest);
|
|
126
|
+
if (await hasPlanningContent(nearest)) {
|
|
127
|
+
return {
|
|
128
|
+
root: nearest,
|
|
129
|
+
specsDir: path.join(nearest, 'specflow', 'specs'),
|
|
130
|
+
changesDir: path.join(nearest, 'specflow', 'changes'),
|
|
131
|
+
archiveDir: path.join(nearest, 'specflow', 'changes', ARCHIVE_DIR_NAME),
|
|
132
|
+
source: 'nearest',
|
|
133
|
+
diagnostics: config.store
|
|
134
|
+
? [
|
|
135
|
+
{
|
|
136
|
+
severity: 'info',
|
|
137
|
+
code: 'store_pointer_ignored',
|
|
138
|
+
message: `Ignoring store pointer '${config.store}' because local planning content exists.`,
|
|
139
|
+
target: 'config.store',
|
|
140
|
+
},
|
|
141
|
+
]
|
|
142
|
+
: [],
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
if (config.store) {
|
|
146
|
+
const declared = resolveFromStoreId(config.store, registry, 'declared');
|
|
147
|
+
if (declared) {
|
|
148
|
+
return declared;
|
|
149
|
+
}
|
|
150
|
+
return {
|
|
151
|
+
...buildImplicitFailure(nearest),
|
|
152
|
+
source: 'declared',
|
|
153
|
+
storeId: config.store,
|
|
154
|
+
diagnostics: [
|
|
155
|
+
{
|
|
156
|
+
severity: 'error',
|
|
157
|
+
code: 'store_not_registered',
|
|
158
|
+
message: `Declared store '${config.store}' is not registered.`,
|
|
159
|
+
target: 'config.store',
|
|
160
|
+
fix: `Run specflow store register for '${config.store}'.`,
|
|
161
|
+
},
|
|
162
|
+
],
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
if (globalConfig.defaultStore) {
|
|
167
|
+
const fallback = resolveFromStoreId(globalConfig.defaultStore, registry, 'global_default');
|
|
168
|
+
if (fallback) {
|
|
169
|
+
return fallback;
|
|
170
|
+
}
|
|
171
|
+
return {
|
|
172
|
+
...buildImplicitFailure(startPath),
|
|
173
|
+
source: 'global_default',
|
|
174
|
+
storeId: globalConfig.defaultStore,
|
|
175
|
+
diagnostics: [
|
|
176
|
+
{
|
|
177
|
+
severity: 'error',
|
|
178
|
+
code: 'default_store_missing',
|
|
179
|
+
message: `Global default store '${globalConfig.defaultStore}' is not registered.`,
|
|
180
|
+
target: 'global.defaultStore',
|
|
181
|
+
fix: 'Register the default store or clear defaultStore from global config.',
|
|
182
|
+
},
|
|
183
|
+
],
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
if (nearest && options.allowImplicitRoot !== false) {
|
|
187
|
+
return {
|
|
188
|
+
root: nearest,
|
|
189
|
+
specsDir: path.join(nearest, 'specflow', 'specs'),
|
|
190
|
+
changesDir: path.join(nearest, 'specflow', 'changes'),
|
|
191
|
+
archiveDir: path.join(nearest, 'specflow', 'changes', ARCHIVE_DIR_NAME),
|
|
192
|
+
source: 'nearest',
|
|
193
|
+
diagnostics: [],
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
return buildImplicitFailure(startPath);
|
|
197
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { StoreError } from './foundation.js';
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import type { Diagnostic } from '../diagnostics.js';
|
|
3
|
+
export declare const KEBAB_CASE_STORE_ID: RegExp;
|
|
4
|
+
export declare const StoreIdentitySchema: z.ZodObject<{
|
|
5
|
+
version: z.ZodLiteral<1>;
|
|
6
|
+
id: z.ZodString;
|
|
7
|
+
remote: z.ZodOptional<z.ZodString>;
|
|
8
|
+
}, "strict", z.ZodTypeAny, {
|
|
9
|
+
version: 1;
|
|
10
|
+
id: string;
|
|
11
|
+
remote?: string | undefined;
|
|
12
|
+
}, {
|
|
13
|
+
version: 1;
|
|
14
|
+
id: string;
|
|
15
|
+
remote?: string | undefined;
|
|
16
|
+
}>;
|
|
17
|
+
export type StoreIdentity = z.infer<typeof StoreIdentitySchema>;
|
|
18
|
+
export declare const StoreProvenanceSchema: z.ZodEnum<["managed", "external"]>;
|
|
19
|
+
export declare const StoreBackendSchema: z.ZodObject<{
|
|
20
|
+
type: z.ZodLiteral<"git">;
|
|
21
|
+
local_path: z.ZodString;
|
|
22
|
+
remote: z.ZodOptional<z.ZodString>;
|
|
23
|
+
branch: z.ZodOptional<z.ZodString>;
|
|
24
|
+
}, "strict", z.ZodTypeAny, {
|
|
25
|
+
type: "git";
|
|
26
|
+
local_path: string;
|
|
27
|
+
remote?: string | undefined;
|
|
28
|
+
branch?: string | undefined;
|
|
29
|
+
}, {
|
|
30
|
+
type: "git";
|
|
31
|
+
local_path: string;
|
|
32
|
+
remote?: string | undefined;
|
|
33
|
+
branch?: string | undefined;
|
|
34
|
+
}>;
|
|
35
|
+
export declare const RegistryEntrySchema: z.ZodObject<{
|
|
36
|
+
provenance: z.ZodEnum<["managed", "external"]>;
|
|
37
|
+
backend: z.ZodObject<{
|
|
38
|
+
type: z.ZodLiteral<"git">;
|
|
39
|
+
local_path: z.ZodString;
|
|
40
|
+
remote: z.ZodOptional<z.ZodString>;
|
|
41
|
+
branch: z.ZodOptional<z.ZodString>;
|
|
42
|
+
}, "strict", z.ZodTypeAny, {
|
|
43
|
+
type: "git";
|
|
44
|
+
local_path: string;
|
|
45
|
+
remote?: string | undefined;
|
|
46
|
+
branch?: string | undefined;
|
|
47
|
+
}, {
|
|
48
|
+
type: "git";
|
|
49
|
+
local_path: string;
|
|
50
|
+
remote?: string | undefined;
|
|
51
|
+
branch?: string | undefined;
|
|
52
|
+
}>;
|
|
53
|
+
}, "strict", z.ZodTypeAny, {
|
|
54
|
+
provenance: "managed" | "external";
|
|
55
|
+
backend: {
|
|
56
|
+
type: "git";
|
|
57
|
+
local_path: string;
|
|
58
|
+
remote?: string | undefined;
|
|
59
|
+
branch?: string | undefined;
|
|
60
|
+
};
|
|
61
|
+
}, {
|
|
62
|
+
provenance: "managed" | "external";
|
|
63
|
+
backend: {
|
|
64
|
+
type: "git";
|
|
65
|
+
local_path: string;
|
|
66
|
+
remote?: string | undefined;
|
|
67
|
+
branch?: string | undefined;
|
|
68
|
+
};
|
|
69
|
+
}>;
|
|
70
|
+
export declare const StoreRegistrySchema: z.ZodObject<{
|
|
71
|
+
version: z.ZodLiteral<1>;
|
|
72
|
+
stores: z.ZodRecord<z.ZodString, z.ZodObject<{
|
|
73
|
+
provenance: z.ZodEnum<["managed", "external"]>;
|
|
74
|
+
backend: z.ZodObject<{
|
|
75
|
+
type: z.ZodLiteral<"git">;
|
|
76
|
+
local_path: z.ZodString;
|
|
77
|
+
remote: z.ZodOptional<z.ZodString>;
|
|
78
|
+
branch: z.ZodOptional<z.ZodString>;
|
|
79
|
+
}, "strict", z.ZodTypeAny, {
|
|
80
|
+
type: "git";
|
|
81
|
+
local_path: string;
|
|
82
|
+
remote?: string | undefined;
|
|
83
|
+
branch?: string | undefined;
|
|
84
|
+
}, {
|
|
85
|
+
type: "git";
|
|
86
|
+
local_path: string;
|
|
87
|
+
remote?: string | undefined;
|
|
88
|
+
branch?: string | undefined;
|
|
89
|
+
}>;
|
|
90
|
+
}, "strict", z.ZodTypeAny, {
|
|
91
|
+
provenance: "managed" | "external";
|
|
92
|
+
backend: {
|
|
93
|
+
type: "git";
|
|
94
|
+
local_path: string;
|
|
95
|
+
remote?: string | undefined;
|
|
96
|
+
branch?: string | undefined;
|
|
97
|
+
};
|
|
98
|
+
}, {
|
|
99
|
+
provenance: "managed" | "external";
|
|
100
|
+
backend: {
|
|
101
|
+
type: "git";
|
|
102
|
+
local_path: string;
|
|
103
|
+
remote?: string | undefined;
|
|
104
|
+
branch?: string | undefined;
|
|
105
|
+
};
|
|
106
|
+
}>>;
|
|
107
|
+
}, "strict", z.ZodTypeAny, {
|
|
108
|
+
stores: Record<string, {
|
|
109
|
+
provenance: "managed" | "external";
|
|
110
|
+
backend: {
|
|
111
|
+
type: "git";
|
|
112
|
+
local_path: string;
|
|
113
|
+
remote?: string | undefined;
|
|
114
|
+
branch?: string | undefined;
|
|
115
|
+
};
|
|
116
|
+
}>;
|
|
117
|
+
version: 1;
|
|
118
|
+
}, {
|
|
119
|
+
stores: Record<string, {
|
|
120
|
+
provenance: "managed" | "external";
|
|
121
|
+
backend: {
|
|
122
|
+
type: "git";
|
|
123
|
+
local_path: string;
|
|
124
|
+
remote?: string | undefined;
|
|
125
|
+
branch?: string | undefined;
|
|
126
|
+
};
|
|
127
|
+
}>;
|
|
128
|
+
version: 1;
|
|
129
|
+
}>;
|
|
130
|
+
export type StoreRegistry = z.infer<typeof StoreRegistrySchema>;
|
|
131
|
+
export type RegistryEntry = z.infer<typeof RegistryEntrySchema>;
|
|
132
|
+
export declare class StoreError extends Error {
|
|
133
|
+
readonly diagnostics: readonly Diagnostic[];
|
|
134
|
+
constructor(message: string, diagnostics: Diagnostic | readonly Diagnostic[]);
|
|
135
|
+
}
|
|
136
|
+
export declare function validateStoreId(id: string): void;
|
|
137
|
+
export declare function canonicalizePath(targetPath: string): string;
|
|
138
|
+
export declare function pathsReferToSameCheckout(leftPath: string, rightPath: string, platform?: NodeJS.Platform): boolean;
|
|
139
|
+
export declare const STORE_IDENTITY_RELATIVE_PATH: string;
|
|
140
|
+
export declare function getStoreIdentityPath(root: string): string;
|
|
141
|
+
export declare function emptyRegistry(): StoreRegistry;
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import * as fs from 'node:fs';
|
|
3
|
+
import * as path from 'node:path';
|
|
4
|
+
export const KEBAB_CASE_STORE_ID = /^[a-z][a-z0-9]*(-[a-z0-9]+)*$/;
|
|
5
|
+
export const StoreIdentitySchema = z
|
|
6
|
+
.object({
|
|
7
|
+
version: z.literal(1),
|
|
8
|
+
id: z.string().regex(KEBAB_CASE_STORE_ID),
|
|
9
|
+
remote: z.string().url().optional(),
|
|
10
|
+
})
|
|
11
|
+
.strict();
|
|
12
|
+
export const StoreProvenanceSchema = z.enum(['managed', 'external']);
|
|
13
|
+
export const StoreBackendSchema = z
|
|
14
|
+
.object({
|
|
15
|
+
type: z.literal('git'),
|
|
16
|
+
local_path: z.string().min(1),
|
|
17
|
+
remote: z.string().optional(),
|
|
18
|
+
branch: z.string().optional(),
|
|
19
|
+
})
|
|
20
|
+
.strict();
|
|
21
|
+
export const RegistryEntrySchema = z
|
|
22
|
+
.object({
|
|
23
|
+
provenance: StoreProvenanceSchema,
|
|
24
|
+
backend: StoreBackendSchema,
|
|
25
|
+
})
|
|
26
|
+
.strict();
|
|
27
|
+
export const StoreRegistrySchema = z
|
|
28
|
+
.object({
|
|
29
|
+
version: z.literal(1),
|
|
30
|
+
stores: z.record(RegistryEntrySchema),
|
|
31
|
+
})
|
|
32
|
+
.strict();
|
|
33
|
+
export class StoreError extends Error {
|
|
34
|
+
diagnostics;
|
|
35
|
+
constructor(message, diagnostics) {
|
|
36
|
+
super(message);
|
|
37
|
+
this.name = 'StoreError';
|
|
38
|
+
this.diagnostics = Array.isArray(diagnostics) ? diagnostics : [diagnostics];
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
export function validateStoreId(id) {
|
|
42
|
+
if (!KEBAB_CASE_STORE_ID.test(id)) {
|
|
43
|
+
throw new StoreError(`Invalid store ID '${id}'.`, {
|
|
44
|
+
severity: 'error',
|
|
45
|
+
code: 'invalid_store_id',
|
|
46
|
+
message: `Store ID '${id}' must be kebab-case.`,
|
|
47
|
+
target: 'store.id',
|
|
48
|
+
fix: 'Use lowercase letters, numbers, and single hyphens only.',
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
export function canonicalizePath(targetPath) {
|
|
53
|
+
return fs.realpathSync.native ? fs.realpathSync.native(targetPath) : fs.realpathSync(targetPath);
|
|
54
|
+
}
|
|
55
|
+
export function pathsReferToSameCheckout(leftPath, rightPath, platform = process.platform) {
|
|
56
|
+
const left = canonicalizePath(leftPath);
|
|
57
|
+
const right = canonicalizePath(rightPath);
|
|
58
|
+
if (platform === 'win32') {
|
|
59
|
+
return left.toLowerCase() === right.toLowerCase();
|
|
60
|
+
}
|
|
61
|
+
if (left === right) {
|
|
62
|
+
return true;
|
|
63
|
+
}
|
|
64
|
+
try {
|
|
65
|
+
const leftStat = fs.statSync(left);
|
|
66
|
+
const rightStat = fs.statSync(right);
|
|
67
|
+
return leftStat.dev === rightStat.dev && leftStat.ino === rightStat.ino;
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
return false;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
export const STORE_IDENTITY_RELATIVE_PATH = path.join('.specflow-store', 'store.yaml');
|
|
74
|
+
export function getStoreIdentityPath(root) {
|
|
75
|
+
return path.join(root, STORE_IDENTITY_RELATIVE_PATH);
|
|
76
|
+
}
|
|
77
|
+
export function emptyRegistry() {
|
|
78
|
+
return { version: 1, stores: {} };
|
|
79
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { Diagnostic } from '../diagnostics.js';
|
|
2
|
+
import { type RegistryEntry, type StoreRegistry } from './foundation.js';
|
|
3
|
+
export declare function inspectStoreEntry(storeId: string, entry: RegistryEntry): Promise<readonly Diagnostic[]>;
|
|
4
|
+
export declare function inspectAllStoresHealth(registry: StoreRegistry): Promise<Array<{
|
|
5
|
+
id: string;
|
|
6
|
+
root: string;
|
|
7
|
+
provenance: string;
|
|
8
|
+
diagnostics: Diagnostic[];
|
|
9
|
+
}>>;
|
|
10
|
+
export declare function flattenStoreDoctorDiagnostics(stores: Array<{
|
|
11
|
+
id: string;
|
|
12
|
+
diagnostics: Diagnostic[];
|
|
13
|
+
}>): Diagnostic[];
|