@fission-ai/openspec 0.17.2 → 0.18.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/dist/cli/index.js +7 -1
- package/dist/commands/artifact-workflow.d.ts +17 -0
- package/dist/commands/artifact-workflow.js +818 -0
- package/dist/core/archive.d.ts +0 -5
- package/dist/core/archive.js +4 -257
- package/dist/core/artifact-graph/graph.d.ts +56 -0
- package/dist/core/artifact-graph/graph.js +141 -0
- package/dist/core/artifact-graph/index.d.ts +7 -0
- package/dist/core/artifact-graph/index.js +13 -0
- package/dist/core/artifact-graph/instruction-loader.d.ts +130 -0
- package/dist/core/artifact-graph/instruction-loader.js +173 -0
- package/dist/core/artifact-graph/resolver.d.ts +61 -0
- package/dist/core/artifact-graph/resolver.js +187 -0
- package/dist/core/artifact-graph/schema.d.ts +13 -0
- package/dist/core/artifact-graph/schema.js +108 -0
- package/dist/core/artifact-graph/state.d.ts +12 -0
- package/dist/core/artifact-graph/state.js +54 -0
- package/dist/core/artifact-graph/types.d.ts +45 -0
- package/dist/core/artifact-graph/types.js +43 -0
- package/dist/core/converters/json-converter.js +2 -1
- package/dist/core/global-config.d.ts +10 -0
- package/dist/core/global-config.js +28 -0
- package/dist/core/index.d.ts +1 -1
- package/dist/core/index.js +1 -1
- package/dist/core/list.d.ts +6 -1
- package/dist/core/list.js +88 -6
- package/dist/core/specs-apply.d.ts +73 -0
- package/dist/core/specs-apply.js +384 -0
- package/dist/core/templates/skill-templates.d.ts +76 -0
- package/dist/core/templates/skill-templates.js +1472 -0
- package/dist/core/update.js +1 -1
- package/dist/core/validation/validator.js +2 -1
- package/dist/core/view.js +28 -8
- package/dist/utils/change-metadata.d.ts +47 -0
- package/dist/utils/change-metadata.js +130 -0
- package/dist/utils/change-utils.d.ts +51 -0
- package/dist/utils/change-utils.js +100 -0
- package/dist/utils/file-system.d.ts +5 -0
- package/dist/utils/file-system.js +7 -0
- package/dist/utils/index.d.ts +3 -1
- package/dist/utils/index.js +4 -1
- package/package.json +4 -1
- package/schemas/spec-driven/schema.yaml +148 -0
- package/schemas/spec-driven/templates/design.md +19 -0
- package/schemas/spec-driven/templates/proposal.md +23 -0
- package/schemas/spec-driven/templates/spec.md +8 -0
- package/schemas/spec-driven/templates/tasks.md +9 -0
- package/schemas/tdd/schema.yaml +213 -0
- package/schemas/tdd/templates/docs.md +15 -0
- package/schemas/tdd/templates/implementation.md +11 -0
- package/schemas/tdd/templates/spec.md +11 -0
- package/schemas/tdd/templates/test.md +11 -0
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
// Artifact definition schema
|
|
3
|
+
export const ArtifactSchema = z.object({
|
|
4
|
+
id: z.string().min(1, { error: 'Artifact ID is required' }),
|
|
5
|
+
generates: z.string().min(1, { error: 'generates field is required' }),
|
|
6
|
+
description: z.string(),
|
|
7
|
+
template: z.string().min(1, { error: 'template field is required' }),
|
|
8
|
+
instruction: z.string().optional(),
|
|
9
|
+
requires: z.array(z.string()).default([]),
|
|
10
|
+
});
|
|
11
|
+
// Apply phase configuration for schema-aware apply instructions
|
|
12
|
+
export const ApplyPhaseSchema = z.object({
|
|
13
|
+
// Artifact IDs that must exist before apply is available
|
|
14
|
+
requires: z.array(z.string()).min(1, { error: 'At least one required artifact' }),
|
|
15
|
+
// Path to file with checkboxes for progress (relative to change dir), or null if no tracking
|
|
16
|
+
tracks: z.string().nullable().optional(),
|
|
17
|
+
// Custom guidance for the apply phase
|
|
18
|
+
instruction: z.string().optional(),
|
|
19
|
+
});
|
|
20
|
+
// Full schema YAML structure
|
|
21
|
+
export const SchemaYamlSchema = z.object({
|
|
22
|
+
name: z.string().min(1, { error: 'Schema name is required' }),
|
|
23
|
+
version: z.number().int().positive({ error: 'Version must be a positive integer' }),
|
|
24
|
+
description: z.string().optional(),
|
|
25
|
+
artifacts: z.array(ArtifactSchema).min(1, { error: 'At least one artifact required' }),
|
|
26
|
+
// Optional apply phase configuration (for schema-aware apply instructions)
|
|
27
|
+
apply: ApplyPhaseSchema.optional(),
|
|
28
|
+
});
|
|
29
|
+
// Per-change metadata schema
|
|
30
|
+
// Note: schema field is validated at parse time against available schemas
|
|
31
|
+
// using a lazy import to avoid circular dependencies
|
|
32
|
+
export const ChangeMetadataSchema = z.object({
|
|
33
|
+
// Required: which workflow schema this change uses
|
|
34
|
+
schema: z.string().min(1, { message: 'schema is required' }),
|
|
35
|
+
// Optional: creation timestamp (ISO date string)
|
|
36
|
+
created: z
|
|
37
|
+
.string()
|
|
38
|
+
.regex(/^\d{4}-\d{2}-\d{2}$/, {
|
|
39
|
+
message: 'created must be YYYY-MM-DD format',
|
|
40
|
+
})
|
|
41
|
+
.optional(),
|
|
42
|
+
});
|
|
43
|
+
//# sourceMappingURL=types.js.map
|
|
@@ -2,6 +2,7 @@ import { readFileSync } from 'fs';
|
|
|
2
2
|
import path from 'path';
|
|
3
3
|
import { MarkdownParser } from '../parsers/markdown-parser.js';
|
|
4
4
|
import { ChangeParser } from '../parsers/change-parser.js';
|
|
5
|
+
import { FileSystemUtils } from '../../utils/file-system.js';
|
|
5
6
|
export class JsonConverter {
|
|
6
7
|
convertSpecToJson(filePath) {
|
|
7
8
|
const content = readFileSync(filePath, 'utf-8');
|
|
@@ -33,7 +34,7 @@ export class JsonConverter {
|
|
|
33
34
|
return JSON.stringify(jsonChange, null, 2);
|
|
34
35
|
}
|
|
35
36
|
extractNameFromPath(filePath) {
|
|
36
|
-
const normalizedPath =
|
|
37
|
+
const normalizedPath = FileSystemUtils.toPosixPath(filePath);
|
|
37
38
|
const parts = normalizedPath.split('/');
|
|
38
39
|
for (let i = parts.length - 1; i >= 0; i--) {
|
|
39
40
|
if (parts[i] === 'specs' || parts[i] === 'changes') {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
export declare const GLOBAL_CONFIG_DIR_NAME = "openspec";
|
|
2
2
|
export declare const GLOBAL_CONFIG_FILE_NAME = "config.json";
|
|
3
|
+
export declare const GLOBAL_DATA_DIR_NAME = "openspec";
|
|
3
4
|
export interface GlobalConfig {
|
|
4
5
|
featureFlags?: Record<string, boolean>;
|
|
5
6
|
}
|
|
@@ -11,6 +12,15 @@ export interface GlobalConfig {
|
|
|
11
12
|
* - Windows fallback: %APPDATA%/openspec/
|
|
12
13
|
*/
|
|
13
14
|
export declare function getGlobalConfigDir(): string;
|
|
15
|
+
/**
|
|
16
|
+
* Gets the global data directory path following XDG Base Directory Specification.
|
|
17
|
+
* Used for user data like schema overrides.
|
|
18
|
+
*
|
|
19
|
+
* - All platforms: $XDG_DATA_HOME/openspec/ if XDG_DATA_HOME is set
|
|
20
|
+
* - Unix/macOS fallback: ~/.local/share/openspec/
|
|
21
|
+
* - Windows fallback: %LOCALAPPDATA%/openspec/
|
|
22
|
+
*/
|
|
23
|
+
export declare function getGlobalDataDir(): string;
|
|
14
24
|
/**
|
|
15
25
|
* Gets the path to the global config file.
|
|
16
26
|
*/
|
|
@@ -4,6 +4,7 @@ import * as os from 'node:os';
|
|
|
4
4
|
// Constants
|
|
5
5
|
export const GLOBAL_CONFIG_DIR_NAME = 'openspec';
|
|
6
6
|
export const GLOBAL_CONFIG_FILE_NAME = 'config.json';
|
|
7
|
+
export const GLOBAL_DATA_DIR_NAME = 'openspec';
|
|
7
8
|
const DEFAULT_CONFIG = {
|
|
8
9
|
featureFlags: {}
|
|
9
10
|
};
|
|
@@ -33,6 +34,33 @@ export function getGlobalConfigDir() {
|
|
|
33
34
|
// Unix/macOS fallback: ~/.config
|
|
34
35
|
return path.join(os.homedir(), '.config', GLOBAL_CONFIG_DIR_NAME);
|
|
35
36
|
}
|
|
37
|
+
/**
|
|
38
|
+
* Gets the global data directory path following XDG Base Directory Specification.
|
|
39
|
+
* Used for user data like schema overrides.
|
|
40
|
+
*
|
|
41
|
+
* - All platforms: $XDG_DATA_HOME/openspec/ if XDG_DATA_HOME is set
|
|
42
|
+
* - Unix/macOS fallback: ~/.local/share/openspec/
|
|
43
|
+
* - Windows fallback: %LOCALAPPDATA%/openspec/
|
|
44
|
+
*/
|
|
45
|
+
export function getGlobalDataDir() {
|
|
46
|
+
// XDG_DATA_HOME takes precedence on all platforms when explicitly set
|
|
47
|
+
const xdgDataHome = process.env.XDG_DATA_HOME;
|
|
48
|
+
if (xdgDataHome) {
|
|
49
|
+
return path.join(xdgDataHome, GLOBAL_DATA_DIR_NAME);
|
|
50
|
+
}
|
|
51
|
+
const platform = os.platform();
|
|
52
|
+
if (platform === 'win32') {
|
|
53
|
+
// Windows: use %LOCALAPPDATA%
|
|
54
|
+
const localAppData = process.env.LOCALAPPDATA;
|
|
55
|
+
if (localAppData) {
|
|
56
|
+
return path.join(localAppData, GLOBAL_DATA_DIR_NAME);
|
|
57
|
+
}
|
|
58
|
+
// Fallback for Windows if LOCALAPPDATA is not set
|
|
59
|
+
return path.join(os.homedir(), 'AppData', 'Local', GLOBAL_DATA_DIR_NAME);
|
|
60
|
+
}
|
|
61
|
+
// Unix/macOS fallback: ~/.local/share
|
|
62
|
+
return path.join(os.homedir(), '.local', 'share', GLOBAL_DATA_DIR_NAME);
|
|
63
|
+
}
|
|
36
64
|
/**
|
|
37
65
|
* Gets the path to the global config file.
|
|
38
66
|
*/
|
package/dist/core/index.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export { GLOBAL_CONFIG_DIR_NAME, GLOBAL_CONFIG_FILE_NAME, type GlobalConfig, getGlobalConfigDir, getGlobalConfigPath, getGlobalConfig, saveGlobalConfig } from './global-config.js';
|
|
1
|
+
export { GLOBAL_CONFIG_DIR_NAME, GLOBAL_CONFIG_FILE_NAME, GLOBAL_DATA_DIR_NAME, type GlobalConfig, getGlobalConfigDir, getGlobalConfigPath, getGlobalConfig, saveGlobalConfig, getGlobalDataDir } from './global-config.js';
|
|
2
2
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/core/index.js
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
// Core OpenSpec logic will be implemented here
|
|
2
|
-
export { GLOBAL_CONFIG_DIR_NAME, GLOBAL_CONFIG_FILE_NAME, getGlobalConfigDir, getGlobalConfigPath, getGlobalConfig, saveGlobalConfig } from './global-config.js';
|
|
2
|
+
export { GLOBAL_CONFIG_DIR_NAME, GLOBAL_CONFIG_FILE_NAME, GLOBAL_DATA_DIR_NAME, getGlobalConfigDir, getGlobalConfigPath, getGlobalConfig, saveGlobalConfig, getGlobalDataDir } from './global-config.js';
|
|
3
3
|
//# sourceMappingURL=index.js.map
|
package/dist/core/list.d.ts
CHANGED
|
@@ -1,4 +1,9 @@
|
|
|
1
|
+
interface ListOptions {
|
|
2
|
+
sort?: 'recent' | 'name';
|
|
3
|
+
json?: boolean;
|
|
4
|
+
}
|
|
1
5
|
export declare class ListCommand {
|
|
2
|
-
execute(targetPath?: string, mode?: 'changes' | 'specs'): Promise<void>;
|
|
6
|
+
execute(targetPath?: string, mode?: 'changes' | 'specs', options?: ListOptions): Promise<void>;
|
|
3
7
|
}
|
|
8
|
+
export {};
|
|
4
9
|
//# sourceMappingURL=list.d.ts.map
|
package/dist/core/list.js
CHANGED
|
@@ -4,8 +4,64 @@ import { getTaskProgressForChange, formatTaskStatus } from '../utils/task-progre
|
|
|
4
4
|
import { readFileSync } from 'fs';
|
|
5
5
|
import { join } from 'path';
|
|
6
6
|
import { MarkdownParser } from './parsers/markdown-parser.js';
|
|
7
|
+
/**
|
|
8
|
+
* Get the most recent modification time of any file in a directory (recursive).
|
|
9
|
+
* Falls back to the directory's own mtime if no files are found.
|
|
10
|
+
*/
|
|
11
|
+
async function getLastModified(dirPath) {
|
|
12
|
+
let latest = null;
|
|
13
|
+
async function walk(dir) {
|
|
14
|
+
const entries = await fs.readdir(dir, { withFileTypes: true });
|
|
15
|
+
for (const entry of entries) {
|
|
16
|
+
const fullPath = path.join(dir, entry.name);
|
|
17
|
+
if (entry.isDirectory()) {
|
|
18
|
+
await walk(fullPath);
|
|
19
|
+
}
|
|
20
|
+
else {
|
|
21
|
+
const stat = await fs.stat(fullPath);
|
|
22
|
+
if (latest === null || stat.mtime > latest) {
|
|
23
|
+
latest = stat.mtime;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
await walk(dirPath);
|
|
29
|
+
// If no files found, use the directory's own modification time
|
|
30
|
+
if (latest === null) {
|
|
31
|
+
const dirStat = await fs.stat(dirPath);
|
|
32
|
+
return dirStat.mtime;
|
|
33
|
+
}
|
|
34
|
+
return latest;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Format a date as relative time (e.g., "2 hours ago", "3 days ago")
|
|
38
|
+
*/
|
|
39
|
+
function formatRelativeTime(date) {
|
|
40
|
+
const now = new Date();
|
|
41
|
+
const diffMs = now.getTime() - date.getTime();
|
|
42
|
+
const diffSecs = Math.floor(diffMs / 1000);
|
|
43
|
+
const diffMins = Math.floor(diffSecs / 60);
|
|
44
|
+
const diffHours = Math.floor(diffMins / 60);
|
|
45
|
+
const diffDays = Math.floor(diffHours / 24);
|
|
46
|
+
if (diffDays > 30) {
|
|
47
|
+
return date.toLocaleDateString();
|
|
48
|
+
}
|
|
49
|
+
else if (diffDays > 0) {
|
|
50
|
+
return `${diffDays}d ago`;
|
|
51
|
+
}
|
|
52
|
+
else if (diffHours > 0) {
|
|
53
|
+
return `${diffHours}h ago`;
|
|
54
|
+
}
|
|
55
|
+
else if (diffMins > 0) {
|
|
56
|
+
return `${diffMins}m ago`;
|
|
57
|
+
}
|
|
58
|
+
else {
|
|
59
|
+
return 'just now';
|
|
60
|
+
}
|
|
61
|
+
}
|
|
7
62
|
export class ListCommand {
|
|
8
|
-
async execute(targetPath = '.', mode = 'changes') {
|
|
63
|
+
async execute(targetPath = '.', mode = 'changes', options = {}) {
|
|
64
|
+
const { sort = 'recent', json = false } = options;
|
|
9
65
|
if (mode === 'changes') {
|
|
10
66
|
const changesDir = path.join(targetPath, 'openspec', 'changes');
|
|
11
67
|
// Check if changes directory exists
|
|
@@ -21,21 +77,46 @@ export class ListCommand {
|
|
|
21
77
|
.filter(entry => entry.isDirectory() && entry.name !== 'archive')
|
|
22
78
|
.map(entry => entry.name);
|
|
23
79
|
if (changeDirs.length === 0) {
|
|
24
|
-
|
|
80
|
+
if (json) {
|
|
81
|
+
console.log(JSON.stringify({ changes: [] }));
|
|
82
|
+
}
|
|
83
|
+
else {
|
|
84
|
+
console.log('No active changes found.');
|
|
85
|
+
}
|
|
25
86
|
return;
|
|
26
87
|
}
|
|
27
88
|
// Collect information about each change
|
|
28
89
|
const changes = [];
|
|
29
90
|
for (const changeDir of changeDirs) {
|
|
30
91
|
const progress = await getTaskProgressForChange(changesDir, changeDir);
|
|
92
|
+
const changePath = path.join(changesDir, changeDir);
|
|
93
|
+
const lastModified = await getLastModified(changePath);
|
|
31
94
|
changes.push({
|
|
32
95
|
name: changeDir,
|
|
33
96
|
completedTasks: progress.completed,
|
|
34
|
-
totalTasks: progress.total
|
|
97
|
+
totalTasks: progress.total,
|
|
98
|
+
lastModified
|
|
35
99
|
});
|
|
36
100
|
}
|
|
37
|
-
// Sort
|
|
38
|
-
|
|
101
|
+
// Sort by preference (default: recent first)
|
|
102
|
+
if (sort === 'recent') {
|
|
103
|
+
changes.sort((a, b) => b.lastModified.getTime() - a.lastModified.getTime());
|
|
104
|
+
}
|
|
105
|
+
else {
|
|
106
|
+
changes.sort((a, b) => a.name.localeCompare(b.name));
|
|
107
|
+
}
|
|
108
|
+
// JSON output for programmatic use
|
|
109
|
+
if (json) {
|
|
110
|
+
const jsonOutput = changes.map(c => ({
|
|
111
|
+
name: c.name,
|
|
112
|
+
completedTasks: c.completedTasks,
|
|
113
|
+
totalTasks: c.totalTasks,
|
|
114
|
+
lastModified: c.lastModified.toISOString(),
|
|
115
|
+
status: c.totalTasks === 0 ? 'no-tasks' : c.completedTasks === c.totalTasks ? 'complete' : 'in-progress'
|
|
116
|
+
}));
|
|
117
|
+
console.log(JSON.stringify({ changes: jsonOutput }, null, 2));
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
39
120
|
// Display results
|
|
40
121
|
console.log('Changes:');
|
|
41
122
|
const padding = ' ';
|
|
@@ -43,7 +124,8 @@ export class ListCommand {
|
|
|
43
124
|
for (const change of changes) {
|
|
44
125
|
const paddedName = change.name.padEnd(nameWidth);
|
|
45
126
|
const status = formatTaskStatus({ total: change.totalTasks, completed: change.completedTasks });
|
|
46
|
-
|
|
127
|
+
const timeAgo = formatRelativeTime(change.lastModified);
|
|
128
|
+
console.log(`${padding}${paddedName} ${status.padEnd(12)} ${timeAgo}`);
|
|
47
129
|
}
|
|
48
130
|
return;
|
|
49
131
|
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Spec Application Logic
|
|
3
|
+
*
|
|
4
|
+
* Extracted from ArchiveCommand to enable standalone spec application.
|
|
5
|
+
* Applies delta specs from a change to main specs without archiving.
|
|
6
|
+
*/
|
|
7
|
+
export interface SpecUpdate {
|
|
8
|
+
source: string;
|
|
9
|
+
target: string;
|
|
10
|
+
exists: boolean;
|
|
11
|
+
}
|
|
12
|
+
export interface ApplyResult {
|
|
13
|
+
capability: string;
|
|
14
|
+
added: number;
|
|
15
|
+
modified: number;
|
|
16
|
+
removed: number;
|
|
17
|
+
renamed: number;
|
|
18
|
+
}
|
|
19
|
+
export interface SpecsApplyOutput {
|
|
20
|
+
changeName: string;
|
|
21
|
+
capabilities: ApplyResult[];
|
|
22
|
+
totals: {
|
|
23
|
+
added: number;
|
|
24
|
+
modified: number;
|
|
25
|
+
removed: number;
|
|
26
|
+
renamed: number;
|
|
27
|
+
};
|
|
28
|
+
noChanges: boolean;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Find all delta spec files that need to be applied from a change.
|
|
32
|
+
*/
|
|
33
|
+
export declare function findSpecUpdates(changeDir: string, mainSpecsDir: string): Promise<SpecUpdate[]>;
|
|
34
|
+
/**
|
|
35
|
+
* Build an updated spec by applying delta operations.
|
|
36
|
+
* Returns the rebuilt content and counts of operations.
|
|
37
|
+
*/
|
|
38
|
+
export declare function buildUpdatedSpec(update: SpecUpdate, changeName: string): Promise<{
|
|
39
|
+
rebuilt: string;
|
|
40
|
+
counts: {
|
|
41
|
+
added: number;
|
|
42
|
+
modified: number;
|
|
43
|
+
removed: number;
|
|
44
|
+
renamed: number;
|
|
45
|
+
};
|
|
46
|
+
}>;
|
|
47
|
+
/**
|
|
48
|
+
* Write an updated spec to disk.
|
|
49
|
+
*/
|
|
50
|
+
export declare function writeUpdatedSpec(update: SpecUpdate, rebuilt: string, counts: {
|
|
51
|
+
added: number;
|
|
52
|
+
modified: number;
|
|
53
|
+
removed: number;
|
|
54
|
+
renamed: number;
|
|
55
|
+
}): Promise<void>;
|
|
56
|
+
/**
|
|
57
|
+
* Build a skeleton spec for new capabilities.
|
|
58
|
+
*/
|
|
59
|
+
export declare function buildSpecSkeleton(specFolderName: string, changeName: string): string;
|
|
60
|
+
/**
|
|
61
|
+
* Apply all delta specs from a change to main specs.
|
|
62
|
+
*
|
|
63
|
+
* @param projectRoot - The project root directory
|
|
64
|
+
* @param changeName - The name of the change to apply
|
|
65
|
+
* @param options - Options for the operation
|
|
66
|
+
* @returns Result of the operation with counts
|
|
67
|
+
*/
|
|
68
|
+
export declare function applySpecs(projectRoot: string, changeName: string, options?: {
|
|
69
|
+
dryRun?: boolean;
|
|
70
|
+
skipValidation?: boolean;
|
|
71
|
+
silent?: boolean;
|
|
72
|
+
}): Promise<SpecsApplyOutput>;
|
|
73
|
+
//# sourceMappingURL=specs-apply.d.ts.map
|