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.
@@ -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
+ };