scene-capability-engine 3.3.24 → 3.3.26
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/CHANGELOG.md +10 -0
- package/bin/scene-capability-engine.js +12 -0
- package/docs/command-reference.md +37 -1
- package/lib/commands/session.js +27 -0
- package/lib/commands/spec-related.js +70 -0
- package/lib/commands/studio.js +66 -12
- package/lib/commands/timeline.js +287 -0
- package/lib/runtime/project-timeline.js +598 -0
- package/lib/spec/related-specs.js +260 -0
- package/package.json +1 -1
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
const { spawnSync } = require('child_process');
|
|
2
|
+
const chalk = require('chalk');
|
|
3
|
+
const fs = require('fs-extra');
|
|
4
|
+
const {
|
|
5
|
+
ProjectTimelineStore,
|
|
6
|
+
captureTimelineCheckpoint
|
|
7
|
+
} = require('../runtime/project-timeline');
|
|
8
|
+
|
|
9
|
+
function normalizeText(value) {
|
|
10
|
+
if (typeof value !== 'string') {
|
|
11
|
+
return '';
|
|
12
|
+
}
|
|
13
|
+
return value.trim();
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function normalizePositiveInteger(value, fallback, max = 10000) {
|
|
17
|
+
const parsed = Number.parseInt(`${value}`, 10);
|
|
18
|
+
if (!Number.isFinite(parsed) || parsed <= 0) {
|
|
19
|
+
return fallback;
|
|
20
|
+
}
|
|
21
|
+
return Math.min(parsed, max);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function normalizeBoolean(value, fallback = false) {
|
|
25
|
+
if (typeof value === 'boolean') {
|
|
26
|
+
return value;
|
|
27
|
+
}
|
|
28
|
+
const normalized = normalizeText(`${value || ''}`).toLowerCase();
|
|
29
|
+
if (!normalized) {
|
|
30
|
+
return fallback;
|
|
31
|
+
}
|
|
32
|
+
if (['1', 'true', 'yes', 'y', 'on'].includes(normalized)) {
|
|
33
|
+
return true;
|
|
34
|
+
}
|
|
35
|
+
if (['0', 'false', 'no', 'n', 'off'].includes(normalized)) {
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
38
|
+
return fallback;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function createStore(dependencies = {}) {
|
|
42
|
+
const projectPath = dependencies.projectPath || process.cwd();
|
|
43
|
+
const fileSystem = dependencies.fileSystem || fs;
|
|
44
|
+
return dependencies.timelineStore || new ProjectTimelineStore(projectPath, fileSystem);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function printPayload(payload, asJson = false, title = 'Timeline') {
|
|
48
|
+
if (asJson) {
|
|
49
|
+
console.log(JSON.stringify(payload, null, 2));
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
console.log(chalk.blue(title));
|
|
54
|
+
if (payload.mode) {
|
|
55
|
+
console.log(` Mode: ${payload.mode}`);
|
|
56
|
+
}
|
|
57
|
+
if (payload.snapshot && payload.snapshot.snapshot_id) {
|
|
58
|
+
console.log(` Snapshot: ${payload.snapshot.snapshot_id}`);
|
|
59
|
+
}
|
|
60
|
+
if (payload.snapshot_id) {
|
|
61
|
+
console.log(` Snapshot: ${payload.snapshot_id}`);
|
|
62
|
+
}
|
|
63
|
+
if (payload.restored_from) {
|
|
64
|
+
console.log(` Restored From: ${payload.restored_from}`);
|
|
65
|
+
}
|
|
66
|
+
if (typeof payload.total === 'number') {
|
|
67
|
+
console.log(` Total: ${payload.total}`);
|
|
68
|
+
}
|
|
69
|
+
if (Array.isArray(payload.snapshots)) {
|
|
70
|
+
for (const item of payload.snapshots) {
|
|
71
|
+
console.log(` - ${item.snapshot_id} | ${item.trigger} | ${item.created_at} | files=${item.file_count}`);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
if (payload.created === false && payload.reason) {
|
|
75
|
+
console.log(` Skipped: ${payload.reason}`);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
async function runTimelineSaveCommand(options = {}, dependencies = {}) {
|
|
80
|
+
const store = createStore(dependencies);
|
|
81
|
+
const payload = await store.saveSnapshot({
|
|
82
|
+
trigger: normalizeText(options.trigger) || 'manual',
|
|
83
|
+
event: normalizeText(options.event) || 'manual.save',
|
|
84
|
+
summary: normalizeText(options.summary),
|
|
85
|
+
command: normalizeText(options.command)
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
const result = {
|
|
89
|
+
mode: 'timeline-save',
|
|
90
|
+
success: true,
|
|
91
|
+
snapshot: payload
|
|
92
|
+
};
|
|
93
|
+
printPayload(result, options.json, 'Timeline Save');
|
|
94
|
+
return result;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
async function runTimelineAutoCommand(options = {}, dependencies = {}) {
|
|
98
|
+
const store = createStore(dependencies);
|
|
99
|
+
const payload = await store.maybeAutoSnapshot({
|
|
100
|
+
event: normalizeText(options.event) || 'auto.tick',
|
|
101
|
+
summary: normalizeText(options.summary),
|
|
102
|
+
intervalMinutes: options.interval
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
printPayload(payload, options.json, 'Timeline Auto');
|
|
106
|
+
return payload;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
async function runTimelineListCommand(options = {}, dependencies = {}) {
|
|
110
|
+
const store = createStore(dependencies);
|
|
111
|
+
const payload = await store.listSnapshots({
|
|
112
|
+
limit: normalizePositiveInteger(options.limit, 20, 2000),
|
|
113
|
+
trigger: normalizeText(options.trigger)
|
|
114
|
+
});
|
|
115
|
+
printPayload(payload, options.json, 'Timeline List');
|
|
116
|
+
return payload;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
async function runTimelineShowCommand(snapshotId, options = {}, dependencies = {}) {
|
|
120
|
+
const store = createStore(dependencies);
|
|
121
|
+
const payload = await store.getSnapshot(snapshotId);
|
|
122
|
+
printPayload(payload, options.json, 'Timeline Show');
|
|
123
|
+
return payload;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
async function runTimelineRestoreCommand(snapshotId, options = {}, dependencies = {}) {
|
|
127
|
+
const store = createStore(dependencies);
|
|
128
|
+
const payload = await store.restoreSnapshot(snapshotId, {
|
|
129
|
+
prune: normalizeBoolean(options.prune, false),
|
|
130
|
+
preSave: options.preSave !== false
|
|
131
|
+
});
|
|
132
|
+
printPayload(payload, options.json, 'Timeline Restore');
|
|
133
|
+
return payload;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
async function runTimelineConfigCommand(options = {}, dependencies = {}) {
|
|
137
|
+
const store = createStore(dependencies);
|
|
138
|
+
|
|
139
|
+
const patch = {};
|
|
140
|
+
if (typeof options.enabled !== 'undefined') {
|
|
141
|
+
patch.enabled = normalizeBoolean(options.enabled, true);
|
|
142
|
+
}
|
|
143
|
+
if (typeof options.interval !== 'undefined') {
|
|
144
|
+
patch.auto_interval_minutes = normalizePositiveInteger(options.interval, 30, 24 * 60);
|
|
145
|
+
}
|
|
146
|
+
if (typeof options.maxEntries !== 'undefined') {
|
|
147
|
+
patch.max_entries = normalizePositiveInteger(options.maxEntries, 120, 10000);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const hasPatch = Object.keys(patch).length > 0;
|
|
151
|
+
const payload = hasPatch
|
|
152
|
+
? await store.updateConfig(patch)
|
|
153
|
+
: await store.getConfig();
|
|
154
|
+
|
|
155
|
+
const result = {
|
|
156
|
+
mode: 'timeline-config',
|
|
157
|
+
success: true,
|
|
158
|
+
updated: hasPatch,
|
|
159
|
+
config: payload
|
|
160
|
+
};
|
|
161
|
+
printPayload(result, options.json, 'Timeline Config');
|
|
162
|
+
return result;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
async function runTimelinePushCommand(gitArgs = [], options = {}, dependencies = {}) {
|
|
166
|
+
const projectPath = dependencies.projectPath || process.cwd();
|
|
167
|
+
|
|
168
|
+
const checkpoint = await captureTimelineCheckpoint({
|
|
169
|
+
trigger: 'push',
|
|
170
|
+
event: 'git.push.preflight',
|
|
171
|
+
summary: normalizeText(options.summary) || 'pre-push timeline checkpoint',
|
|
172
|
+
command: `git push ${Array.isArray(gitArgs) ? gitArgs.join(' ') : ''}`.trim()
|
|
173
|
+
}, {
|
|
174
|
+
projectPath,
|
|
175
|
+
fileSystem: dependencies.fileSystem
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
const result = spawnSync('git', ['push', ...(Array.isArray(gitArgs) ? gitArgs : [])], {
|
|
179
|
+
cwd: projectPath,
|
|
180
|
+
stdio: 'inherit',
|
|
181
|
+
windowsHide: true
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
const statusCode = Number.isInteger(result.status) ? result.status : 1;
|
|
185
|
+
if (statusCode !== 0) {
|
|
186
|
+
const error = new Error(`git push failed with exit code ${statusCode}`);
|
|
187
|
+
error.exitCode = statusCode;
|
|
188
|
+
throw error;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const payload = {
|
|
192
|
+
mode: 'timeline-push',
|
|
193
|
+
success: true,
|
|
194
|
+
checkpoint,
|
|
195
|
+
command: `git push ${Array.isArray(gitArgs) ? gitArgs.join(' ') : ''}`.trim()
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
printPayload(payload, options.json, 'Timeline Push');
|
|
199
|
+
return payload;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
async function safeRun(handler, options = {}, ...args) {
|
|
203
|
+
try {
|
|
204
|
+
await handler(...args, options);
|
|
205
|
+
} catch (error) {
|
|
206
|
+
if (options.json) {
|
|
207
|
+
console.log(JSON.stringify({ success: false, error: error.message }, null, 2));
|
|
208
|
+
} else {
|
|
209
|
+
console.error(chalk.red('Timeline command failed:'), error.message);
|
|
210
|
+
}
|
|
211
|
+
process.exitCode = error.exitCode || 1;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function registerTimelineCommands(program) {
|
|
216
|
+
const timeline = program
|
|
217
|
+
.command('timeline')
|
|
218
|
+
.description('Project local timeline snapshots (auto/key-event/manual/restore)');
|
|
219
|
+
|
|
220
|
+
timeline
|
|
221
|
+
.command('save')
|
|
222
|
+
.description('Create a manual timeline snapshot')
|
|
223
|
+
.option('--trigger <trigger>', 'Trigger label', 'manual')
|
|
224
|
+
.option('--event <event>', 'Event label', 'manual.save')
|
|
225
|
+
.option('--summary <text>', 'Summary for this checkpoint')
|
|
226
|
+
.option('--command <text>', 'Command context label')
|
|
227
|
+
.option('--json', 'Output as JSON')
|
|
228
|
+
.action(async (options) => safeRun(runTimelineSaveCommand, options));
|
|
229
|
+
|
|
230
|
+
timeline
|
|
231
|
+
.command('auto')
|
|
232
|
+
.description('Run interval-based auto timeline snapshot check')
|
|
233
|
+
.option('--interval <minutes>', 'Override auto interval minutes')
|
|
234
|
+
.option('--event <event>', 'Event label', 'auto.tick')
|
|
235
|
+
.option('--summary <text>', 'Summary for auto checkpoint')
|
|
236
|
+
.option('--json', 'Output as JSON')
|
|
237
|
+
.action(async (options) => safeRun(runTimelineAutoCommand, options));
|
|
238
|
+
|
|
239
|
+
timeline
|
|
240
|
+
.command('list')
|
|
241
|
+
.description('List timeline snapshots')
|
|
242
|
+
.option('--limit <n>', 'Maximum snapshots', '20')
|
|
243
|
+
.option('--trigger <trigger>', 'Filter by trigger')
|
|
244
|
+
.option('--json', 'Output as JSON')
|
|
245
|
+
.action(async (options) => safeRun(runTimelineListCommand, options));
|
|
246
|
+
|
|
247
|
+
timeline
|
|
248
|
+
.command('show <snapshotId>')
|
|
249
|
+
.description('Show one timeline snapshot')
|
|
250
|
+
.option('--json', 'Output as JSON')
|
|
251
|
+
.action(async (snapshotId, options) => safeRun(runTimelineShowCommand, options, snapshotId));
|
|
252
|
+
|
|
253
|
+
timeline
|
|
254
|
+
.command('restore <snapshotId>')
|
|
255
|
+
.description('Restore workspace from a timeline snapshot')
|
|
256
|
+
.option('--prune', 'Delete files not present in snapshot (dangerous)')
|
|
257
|
+
.option('--no-pre-save', 'Do not create a pre-restore snapshot')
|
|
258
|
+
.option('--json', 'Output as JSON')
|
|
259
|
+
.action(async (snapshotId, options) => safeRun(runTimelineRestoreCommand, options, snapshotId));
|
|
260
|
+
|
|
261
|
+
timeline
|
|
262
|
+
.command('config')
|
|
263
|
+
.description('Show/update timeline config')
|
|
264
|
+
.option('--enabled <boolean>', 'Enable timeline (true/false)')
|
|
265
|
+
.option('--interval <minutes>', 'Auto snapshot interval in minutes')
|
|
266
|
+
.option('--max-entries <n>', 'Maximum retained snapshots')
|
|
267
|
+
.option('--json', 'Output as JSON')
|
|
268
|
+
.action(async (options) => safeRun(runTimelineConfigCommand, options));
|
|
269
|
+
|
|
270
|
+
timeline
|
|
271
|
+
.command('push [gitArgs...]')
|
|
272
|
+
.description('Create a pre-push timeline snapshot, then run git push')
|
|
273
|
+
.option('--summary <text>', 'Summary for pre-push checkpoint')
|
|
274
|
+
.option('--json', 'Output as JSON')
|
|
275
|
+
.action(async (gitArgs, options) => safeRun(runTimelinePushCommand, options, gitArgs));
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
module.exports = {
|
|
279
|
+
runTimelineSaveCommand,
|
|
280
|
+
runTimelineAutoCommand,
|
|
281
|
+
runTimelineListCommand,
|
|
282
|
+
runTimelineShowCommand,
|
|
283
|
+
runTimelineRestoreCommand,
|
|
284
|
+
runTimelineConfigCommand,
|
|
285
|
+
runTimelinePushCommand,
|
|
286
|
+
registerTimelineCommands
|
|
287
|
+
};
|