Setting the file. One moment.
Replatform · Wix Replatform · wix/skills · Skills Docs
ContentsBack to the top of the page async function tailCommand
— line 164
This file
Number 45.28
Position 28 of 182
Type JavaScript
Size 36 KB
Lines 456 scripts/ replatform.js
JavaScript · 456 lines · 36 KB
===
'ERR_REQUIRE_ESM'
)) {
11 process.stderr. write ( `replatform: cannot load the bundled commander (${ error . code }). ${ require ( '../lib/toolkit-dependencies.js' ). FIX } \n ` );
12 process. exit ( 1 );
13 }
14 throw error;
15 }
16 }
17
18 const fs = require ( 'node:fs/promises' );
19 const fssync = require ( 'node:fs' );
20 const path = require ( 'node:path' );
21 const { spawn } = require ( 'node:child_process' );
22 const { Command , Option } = requireCommander ();
23 const {
24 RunController , readActive , findRun , requestCancellation , defaultEventsPath , readJson , exitCode ,
25 } = require ( '../lib/run-controller.js' );
26 const { loadArtifacts } = require ( '../lib/orchestration-state.js' );
27 const { determineNextStep } = require ( '../lib/orchestration-router.js' );
28 const { listUserActions , readUserAction , answerUserAction } = require ( '../lib/user-actions.js' );
29 const { writeJsonAtomic } = require ( '../lib/orchestration-state.js' );
30 const { chooseAdapter } = require ( '../lib/agent-adapters/index.js' );
31 const { defaultProbeWixCli } = require ( '../lib/orchestration-preflight.js' );
32 const { readTaskState } = require ( '../lib/task-state.js' );
33 const { applyUserActionEffect } = require ( '../lib/user-action-effects.js' );
34
35 const VERSION = '0.1.0' ;
36
37 function duration ( value , { allowOff = false , min = 0 , max = Infinity } = {}) {
38 if (allowOff && (value === undefined || value === 'off' )) return null ;
39 if ( ! / ^ \d + (ms | s | m | h | d) $ / . test ( String (value))) throw new Error ( `invalid duration: ${ value }` );
40 const [, amount , unit ] = String (value). match ( / ^ ( \d + )(ms | s | m | h | d) $ / );
41 const multiplier = { ms: 1 , s: 1000 , m: 60000 , h: 3600000 , d: 86400000 }[unit];
42 const result = Number (amount) * multiplier;
43 if (result < min || result > max) throw new Error ( `duration out of allowed range: ${ value }` );
44 return result;
45 }
46
47 function positive ( value , name , fallback ) {
48 if (value === undefined ) return fallback;
49 if ( ! / ^ \d +$ / . test ( String (value)) || Number (value) < 1 ) throw new Error ( `${ name } must be a positive integer` );
50 return Number (value);
51 }
52
53 function output ( value , format = 'text' , text = null ) {
54 if (format === 'json' ) process.stdout. write ( `${ JSON . stringify ( value ) } \n ` );
55 else if (format === 'jsonl' ) process.stdout. write ( `${ JSON . stringify ( value ) } \n ` );
56 else process.stdout. write ( `${ text || JSON . stringify ( value , null , 2 ) } \n ` );
57 }
58
59 function rootDir () { return path. resolve (process.env. REPLATFORM_MIGRATIONS_DIR || path. join (process. cwd (), 'migrations' )); }
60 async function resolveProject ( value ) {
61 if ( ! value) throw new Error ( 'project is required (or set --project)' );
62 if ( / ^ rpr_ [0-9a-f-] +$ / . test (value)) {
63 const record = await findRun ( rootDir (), value);
64 if ( ! record) throw new Error ( `run ID not found: ${ value }` );
65 return { projectDir: record.projectDir, runId: value };
66 }
67 const candidate = path. resolve (value);
68 // A value with path syntax is a path even before `run` creates its project directory.
69 // Only a bare name is resolved beneath the configured migrations root.
70 if (path. isAbsolute (value) || value. includes (path.sep) || value. startsWith ( '.' )) return { projectDir: candidate, runId: null };
71 if (fssync. existsSync (candidate)) return { projectDir: candidate, runId: null };
72 return { projectDir: path. join ( rootDir (), value), runId: null };
73 }
74 function projectArg ( args , index = 1 ) { return args.project || args._[index]; }
75
76 function runtimeOptions ( args ) {
77 const format = args.format === 'event-stream' ? 'jsonl' : (args.format || 'jsonl' );
78 if ( ! [ 'text' , 'json' , 'jsonl' ]. includes (format)) throw new Error ( 'format must be text, json, jsonl, or event-stream' );
79 const statusIntervalMs = args.statusInterval === 'off' || (args.detach && args.statusInterval === undefined )
80 ? null : duration (args.statusInterval || '60s' , { min: 10000 , max: 300000 });
81 if ( ! [ 'auto' , 'codex' , 'claude' ]. includes (args.agent || 'auto' )) throw new Error ( 'agent must be auto, codex, or claude' );
82 if (args.sourceUrl !== undefined ) {
83 let source;
84 try { source = new URL (args.sourceUrl); } catch { throw new Error ( 'source-url must be an https URL' ); }
85 if (source.protocol !== 'https:' || source.username || source.password) throw new Error ( 'source-url must be a credential-free https URL' );
86 }
87 if (args.sourceMode !== undefined && ! [ 'public_content' , 'authenticated_api' , 'private_data' , 'files_only' ]. includes (args.sourceMode)) throw new Error ( 'invalid source-mode' );
88 if (args.deliveryMode !== undefined && ! [ 'management' , 'website' , 'management_and_website' ]. includes (args.deliveryMode)) throw new Error ( 'invalid delivery-mode' );
89 if (args.managementImportMode !== undefined && ! [ 'standard' , 'quick' ]. includes (args.managementImportMode)) throw new Error ( 'invalid management-import-mode' );
90 if (args.destination !== undefined && ! (args.destination === 'new' || / ^ existing: [A-Za-z0-9_-] +$ / . test (args.destination))) throw new Error ( 'destination must be new or existing:<site-id>' );
91 // An explicit new-site type, and Harmony's optional business type.
92 if (args.siteType !== undefined && ! [ 'headless' , 'studio' , 'harmony' ]. includes (args.siteType)) throw new Error ( 'site-type must be headless, studio, or harmony' );
93 if (args.siteBusinessType !== undefined && ! [ 'store' , 'bookings' , 'other' ]. includes (args.siteBusinessType)) throw new Error ( 'site-business-type must be store, bookings, or other' );
94 if (args.siteBusinessType !== undefined && args.siteType !== 'harmony' ) throw new Error ( 'site-business-type is valid only with --site-type harmony' );
95 if (args.siteType !== undefined && args.destination !== undefined && args.destination !== 'new' ) throw new Error ( 'site-type applies only to --destination new' );
96 // The existing-site mute answer, given up front (a new site is always muted).
97 if (args.notificationMute !== undefined && ! [ 'on' , 'off' ]. includes (args.notificationMute)) throw new Error ( 'notification-mute must be on or off' );
98 if (args.notificationMute !== undefined && ! / ^ existing:/ . test (args.destination || '' )) throw new Error ( 'notification-mute applies only to --destination existing:<site-id>; a new site is always muted' );
99 return {
100 agent: args.agent || 'auto' , agentPath: args.agentPath, sourceUrl: args.sourceUrl, sourceMode: args.sourceMode,
101 deliveryMode: args.deliveryMode, destination: args.destination, managementImportMode: args.managementImportMode,
102 siteType: args.siteType, siteBusinessType: args.siteBusinessType, notificationMute: args.notificationMute,
103 fileInputPaths: args.file?. length ? args.file. map (( filePath ) => path. resolve (filePath)) : undefined ,
104 oneClick: args.oneClick, dryRun: args.dryRun, eventsFile: args.eventsFile ? path. resolve (args.eventsFile) : null , vendorEvents: args.vendorEvents ? path. resolve (args.vendorEvents) : null ,
105 statusIntervalMs, wallTimeoutMs: duration (args.wallTimeout || 'off' , { allowOff: true , min: 60000 , max: 7 * 86400000 }),
106 idleTimeoutMs: duration (args.idleTimeout || '15m' , { min: 60000 , max: 2 * 3600000 }),
107 workerStartupTimeoutMs: duration (args.workerStartupTimeout || '120s' , { min: 1000 , max: 3600000 }),
108 maxAgentTurns: positive (args.maxAgentTurns, 'max-agent-turns' , 12 ),
109 maxNoProgressContinuations: positive (args.maxNoProgressContinuations, 'max-no-progress-continuations' , 1 ),
110 format,
111 };
112 }
113
114 function applyApproval ( projectDir , request , answer ) {
115 return applyUserActionEffect (projectDir, request, answer);
116 }
117
118 async function runCommand ( args , { resume = false , resumeBlocked = resume, internalRunId = null } = {}) {
119 const target = await resolveProject ( projectArg (args));
120 const options = runtimeOptions (args);
121 if (resume && [args.sourceUrl, args.sourceMode, args.file, args.deliveryMode, args.destination, args.managementImportMode, args.siteType, args.siteBusinessType, args.notificationMute, args.oneClick]. some (( value ) => value !== undefined )) throw new Error ( 'resume does not accept intake mutation options' );
122 const controller = new RunController (target.projectDir, { ... options, resume, resumeBlocked, runId: internalRunId || undefined , onEvent : ( event ) => {
123 if (options.format === 'jsonl' ) process.stdout. write ( `${ JSON . stringify ( event ) } \n ` );
124 } });
125 if (args.detach && ! internalRunId) {
126 // The child receives a serialized, already parsed invocation; no shell or prompt is built.
127 const encoded = Buffer. from ( JSON . stringify ({ project: target.projectDir, options: { ... options, resume, resumeBlocked, runId: controller.id } })). toString ( 'base64url' );
128 const child = spawn (process.execPath, [__filename, '__supervise' , encoded], { detached: true , stdio: 'ignore' });
129 child. unref ();
130 const result = { schemaVersion: 1 , runId: controller.id, project: path. basename (target.projectDir), outcome: 'running' , nextAction: `replatform status ${ controller . id }` };
131 // Allocate a lookup record before returning; the supervisor will replace it immediately.
132 await fs. mkdir (path. join (target.projectDir, 'state' , 'runner' , 'runs' ), { recursive: true });
133 await writeJsonAtomic (path. join (target.projectDir, 'state' , 'runner' , 'runs' , `${ controller . id }.json` ), { schemaVersion: 1 , runId: controller.id, projectDir: target.projectDir, pid: child.pid, status: 'starting' , updatedAt: new Date (). toISOString () });
134 output (result, options.format, `Started detached run ${ controller . id }. \n Status: replatform status ${ controller . id }` );
135 return 0 ;
136 }
137 const result = await controller. run ();
138 if (options.format === 'json' ) output (result, 'json' );
139 else if (options.format === 'text' ) output (result, 'text' , `${ result . outcome }: ${ result . router ?. reason || 'run finished'}${ result . requestId ? ` \n Request: ${ result . requestId } \n Answer: ${ result . nextAction }` : result . router ?. reason === 'run_blocked' ? ` \n Recorded blocker: ${ JSON . stringify ( result . router . resumeFrom ) } \n Next: ${ result . nextAction }` : ''}` );
140 return exitCode (result);
141 }
142
143 async function statusCommand ( args ) {
144 const target = await resolveProject ( projectArg (args));
145 const active = await readActive (target.projectDir);
146 const artifacts = await loadArtifacts (target.projectDir);
147 const router = artifacts.run ? await determineNextStep (target.projectDir, artifacts) : null ;
148 const historyDir = path. join (target.projectDir, 'state' , 'runner' , 'runs' );
149 let latestRun = target.runId ? await findRun ( rootDir (), target.runId) : null ;
150 if ( ! latestRun) {
151 try {
152 const records = await Promise . all (( await fs. readdir (historyDir)). filter (( name ) => name. endsWith ( '.json' )). map (( name ) => readJson (path. join (historyDir, name))));
153 latestRun = records. filter (Boolean). sort (( a , b ) => String (b.updatedAt || '' ). localeCompare ( String (a.updatedAt || '' )))[ 0 ] || null ;
154 } catch (error) { if (error.code !== 'ENOENT' ) throw error; }
155 }
156 const eventPath = active?.eventPath || latestRun?.eventPath || defaultEventsPath (target.projectDir);
157 let lastEvent = null ;
158 try { const lines = ( await fs. readFile (eventPath, 'utf8' )). trim (). split ( ' \n ' ). filter (Boolean); if (lines. length ) lastEvent = JSON . parse (lines. at ( - 1 )); } catch (error) { if (error.code !== 'ENOENT' ) throw error; }
159 const result = { schemaVersion: 1 , project: path. basename (target.projectDir), activeRun: active ? { runId: active.runId, pid: active.pid, childPid: active.childPid, status: active.status } : null , orchestration: artifacts.run ? { status: artifacts.run.status, state: artifacts.run.currentState } : null , router, eventStream: { path: eventPath, lastEventId: lastEvent?.eventId || null , lastEventAt: lastEvent?.timestamp || null }, latestResult: latestRun?.result || null };
160 output (result, args.format || 'text' , active ? `Run ${ active . runId }: ${ active . status }` : `No active run. ${ router ?. reason || 'No orchestration state.'}` );
161 return 0 ;
162 }
163
164 async function tailCommand ( args ) {
165 const target = await resolveProject ( projectArg (args));
166 const active = await readActive (target.projectDir);
167 const eventFile = active?.eventPath || defaultEventsPath (target.projectDir);
168 const since = args.since;
169 let passedCursor = ! since || ! / ^ evt_ [0-9a-f-] +$ / . test (since);
170 const shouldPrint = ( item ) => {
171 if ( ! since) return true ;
172 if ( / ^ evt_ [0-9a-f-] +$ / . test (since)) {
173 if ( ! passedCursor && item.eventId === since) { passedCursor = true ; return false ; }
174 return passedCursor;
175 }
176 return Date. parse (item.timestamp) >= Date. now () - duration (since, { min: 0 , max: 365 * 86400000 });
177 };
178 const print = async () => {
179 let text; try { text = await fs. readFile (eventFile, 'utf8' ); } catch (error) { if (error.code === 'ENOENT' ) return false ; throw error; }
180 const lines = text. trim (). split ( ' \n ' ). filter (Boolean);
181 for ( const line of lines) {
182 const item = JSON . parse (line); if ( ! shouldPrint (item)) continue ;
183 if ((args.format || 'jsonl' ) === 'text' ) process.stdout. write ( `${ item . timestamp } ${ item . type }: ${ item . message } \n ` );
184 else process.stdout. write ( `${ line } \n ` );
185 }
186 return lines. length ;
187 };
188 let known = await print ();
189 if ( ! args.follow) return 0 ;
190 while ( true ) {
191 await new Promise (( resolve ) => setTimeout (resolve, 1000 ));
192 let text = '' ; try { text = await fs. readFile (eventFile, 'utf8' ); } catch { continue ; }
193 const lines = text. trim (). split ( ' \n ' ). filter (Boolean);
194 for ( const line of lines. slice (known)) {
195 const item = JSON . parse (line); if ( shouldPrint (item)) process.stdout. write ( `${ line } \n ` );
196 }
197 known = lines. length ;
198 const current = await readActive (target.projectDir); if ( ! current) break ;
199 }
200 return 0 ;
201 }
202
203 function allowedArtifact ( requests , target ) {
204 const normalized = target. replaceAll ( ' \\ ' , '/' );
205 if (normalized. includes ( '..' ) || normalized. startsWith ( '/' ) || / ^ (config | state | source | raw)( \/ |$ )/ . test (normalized)) return false ;
206 return normalized. includes ( '/review/' ) || requests. some (( request ) => request.artifactRefs. some (( ref ) => ref.path === normalized));
207 }
208 async function showCommand ( args ) {
209 const target = await resolveProject ( projectArg (args));
210 const id = args._[ 2 ]; if ( ! id) throw new Error ( 'show requires request-id-or-artifact' );
211 if ( / ^ uar_ [0-9a-f-] +$ / . test (id)) { const request = await readUserAction (target.projectDir, id); output (request, args.format || 'text' ); return 0 ; }
212 const requests = await listUserActions (target.projectDir, {});
213 if ( ! allowedArtifact (requests. map (( row ) => ({ ... row, artifactRefs: [] })), id)) {
214 // Index rows intentionally omit refs; read each action before authorizing an artifact.
215 const full = []; for ( const row of requests) full. push ( await readUserAction (target.projectDir, row.requestId));
216 if ( ! allowedArtifact (full, id)) throw new Error ( 'artifact is not user-reviewable' );
217 }
218 const fullPath = path. resolve (target.projectDir, id);
219 const root = `${ path . resolve ( target . projectDir ) }${ path . sep }` ;
220 if ( ! fullPath. startsWith (root)) throw new Error ( 'artifact escapes project' );
221 const content = await fs. readFile (fullPath);
222 const max = positive (args.maxBytes, 'max-bytes' , 1048576 );
223 const cursor = args.cursor === undefined ? 0 : Number (args.cursor);
224 if ( ! Number. isInteger (cursor) || cursor < 0 || cursor > content. length ) throw new Error ( 'cursor must be a valid byte offset' );
225 const emitted = content. subarray (cursor, cursor + max); const truncated = cursor + emitted. length < content. length ;
226 const fullRequests = []; for ( const row of requests) fullRequests. push ( await readUserAction (target.projectDir, row.requestId));
227 const mediaType = fullRequests. flatMap (( request ) => request.artifactRefs). find (( ref ) => ref.path === id. replaceAll ( ' \\ ' , '/' ))?.mediaType || 'text/plain' ;
228 const result = { schemaVersion: 1 , path: id. replaceAll ( ' \\ ' , '/' ), mediaType, totalBytes: content. length , cursor, emittedBytes: emitted. length , truncated, nextCursor: truncated ? cursor + emitted. length : null , content: emitted. toString ( 'utf8' ) };
229 output (result, args.format || 'text' , `${ result . content }${ truncated ? ` \n TRUNCATED: showed ${ emitted . length }B of ${ content . length }B; rerun with --max-bytes ${ content . length } or larger.` : ''}` );
230 return 0 ;
231 }
232
233 async function requestsCommand ( args ) {
234 const target = await resolveProject ( projectArg (args));
235 const rows = await listUserActions (target.projectDir, { status: args.status || 'pending' });
236 const actions = await Promise . all (rows. map (( row ) => readUserAction (target.projectDir, row.requestId)));
237 const text = actions. map (( action ) => {
238 const answer = action.response.mode === 'choice'
239 ? `--choice <${ action . choices . map (( choice ) => choice . id ). join ( '|' ) }>`
240 : action.response.mode === 'text' ? '--text "<answer>"' : '--acknowledge' ;
241 const choices = action.response.mode === 'choice'
242 ? ` \n Choices: ${ action . choices . map (( choice ) => `${ choice . id } (${ choice . label })` ). join ( ', ' ) }` : '' ;
243 const next = action.response.mode === 'none'
244 ? `After completing it: replatform answer --project ${ target . projectDir } --request-id ${ action . requestId } --acknowledge --continue`
245 : `Answer: replatform answer --project ${ target . projectDir } --request-id ${ action . requestId } ${ answer }` ;
246 const prompt = action.response.mode === 'none' ? ` \n Required action: ${ action . prompt }` : '' ;
247 return `${ action . requestId }: ${ action . title } (${ action . status }) \n Response: ${ action . response . mode }${ choices }${ prompt } \n Show: replatform show --project ${ target . projectDir } ${ action . requestId } \n Next: ${ next }` ;
248 }). join ( ' \n\n ' ) || 'No matching user actions.' ;
249 output (actions, args.format || 'text' , text);
250 return 0 ;
251 }
252
253 async function tasksCommand ( args ) {
254 const target = await resolveProject ( projectArg (args));
255 const state = await readTaskState (target.projectDir);
256 const active = await readActive (target.projectDir);
257 const result = state || { schemaVersion: 1 , runId: active?.runId || target.runId || null , updatedAt: null , tasks: [], agents: [] };
258 output (result, args.format || 'text' , result.tasks. length
259 ? result.tasks. map (( task ) => `${ task . state }: ${ task . title }` ). join ( ' \n ' )
260 : 'No durable work state has been recorded for this run.' );
261 return 0 ;
262 }
263
264 async function answerCommand ( args ) {
265 const target = await resolveProject ( projectArg (args)); const requestId = args._[ 2 ];
266 if ( ! requestId) throw new Error ( 'answer requires request id' );
267 const variants = [args.choice !== undefined , args.text !== undefined , args.textFile !== undefined , args.acknowledge === true ]. filter (Boolean). length ;
268 if (variants !== 1 ) throw new Error ( 'answer requires exactly one of --choice, --text, --text-file, --acknowledge' );
269 if (args.note !== undefined && args.choice === undefined && ! args.acknowledge) throw new Error ( 'note is supported only with a choice or acknowledgement' );
270 const answer = args.acknowledge ? { acknowledged: true , ... (args.note !== undefined ? { note: args.note } : {}) } : args.choice !== undefined ? { choice: args.choice, ... (args.note !== undefined ? { note: args.note } : {}) } : { text: args.textFile ? await fs. readFile (path. resolve (args.textFile), 'utf8' ) : args.text };
271 const result = await answerUserAction (target.projectDir, requestId, answer, { applyEffect : ( request , normalized ) => applyApproval (target.projectDir, request, normalized) });
272 if ( ! args.continue) { output (result, args.format || 'text' , `Answered ${ requestId }. Resume: replatform resume ${ target . projectDir }` ); return 0 ; }
273 args._ = [ 'run' , target.projectDir]; args.project = target.projectDir; return runCommand (args, { resume: true , resumeBlocked: false });
274 }
275
276 async function doctorCommand ( args ) {
277 const value = projectArg (args); const target = value ? await resolveProject (value) : null ;
278 const ids = args.agent && args.agent !== 'auto' ? [args.agent] : [ 'codex' , 'claude' ];
279 const adapters = ids. map (( id ) => chooseAdapter (id, { agentPath: args.agentPath, checkAuth: true }));
280 const active = target ? await readActive (target.projectDir) : null ;
281 const result = { schemaVersion: 1 , node: process.version, project: target?.projectDir || null , writable: target ? await fs. access (target.projectDir, fssync.constants. W_OK ). then (() => true ). catch (() => false ) : null , adapters, activeRun: active };
282 output (result, args.format || 'text' , adapters. map (( a ) => `${ a . id || args . agent }: ${ a . ok ? 'available' : a . reason }` ). join ( ' \n ' ));
283 return adapters. some (( item ) => item.ok) ? 0 : 2 ;
284 }
285
286 async function projectsCommand ( args ) {
287 const root = rootDir ();
288 let entries;
289 try { entries = await fs. readdir (root, { withFileTypes: true }); } catch (error) {
290 if (error.code === 'ENOENT' ) { output ({ schemaVersion: 1 , projects: [] }, args.format || 'text' , 'No migration projects.' ); return 0 ; }
291 throw error;
292 }
293 const projects = [];
294 for ( const entry of entries) {
295 if ( ! entry. isDirectory () || entry.name. startsWith ( '.' )) continue ;
296 const projectDir = path. join (root, entry.name); const artifacts = await loadArtifacts (projectDir);
297 if ( ! artifacts.run && ! fssync. existsSync (path. join (projectDir, 'state' , 'runner' ))) continue ;
298 const active = await readActive (projectDir);
299 const router = artifacts.run ? await determineNextStep (projectDir, artifacts) : null ;
300 projects. push ({ project: entry.name, activeRun: active ? { runId: active.runId, status: active.status } : null , orchestration: artifacts.run ? { status: artifacts.run.status, state: artifacts.run.currentState } : null , router: router ? { state: router.nextState, reason: router.reason } : null });
301 }
302 projects. sort (( a , b ) => a.project. localeCompare (b.project));
303 output ({ schemaVersion: 1 , projects }, args.format || 'text' , projects. map (( item ) => item.project). join ( ' \n ' ) || 'No migration projects.' );
304 return 0 ;
305 }
306
307 async function accountCommand ( args ) {
308 const wixCli = await defaultProbeWixCli ();
309 const result = {
310 schemaVersion: 1 ,
311 available: wixCli.available,
312 authState: wixCli.authState,
313 ... (wixCli.identity || wixCli.email ? { identity: wixCli.identity || wixCli.email } : {}),
314 ... (wixCli.reason ? { reason: wixCli.reason } : {}),
315 };
316 const confirmed = result.authState === 'authenticated' && Boolean (result.identity);
317 output (result, args.format || 'text' , confirmed
318 ? `Authenticated Wix CLI identity: ${ result . identity }`
319 : `Wix CLI identity could not be confirmed: ${ result . reason || 'unknown'}` );
320 return confirmed ? 0 : 2 ;
321 }
322
323 function commandArgs ( command , positional , options ) {
324 return { ... options, _: [command, ... (positional === undefined ? [] : [positional])] };
325 }
326
327 function addRuntimeOptions ( command , { intake = false } = {}) {
328 command
329 . addHelpText ( 'after' , ' \n This command is non-interactive: it never prompts, opens a pager/editor/browser/login, or waits for stdin. \n ' )
330 . addOption ( new Option ( '--agent <id>' , 'auto follows the invoking agent; otherwise selects an installed, authenticated adapter. codex and claude require that exact adapter.' ). choices ([ 'auto' , 'codex' , 'claude' ]). default ( 'auto' ))
331 . option ( '--agent-path <path>' , 'adapter executable override (testing/development)' )
332 . addOption ( new Option ( '--format <format>' , 'jsonl streams lifecycle events (default); json emits one final result; text is human-readable; event-stream aliases jsonl.' ). choices ([ 'text' , 'json' , 'jsonl' , 'event-stream' ]). default ( 'jsonl' ))
333 . option ( '--status-interval <duration>' , 'liveness heartbeat interval: 60s foreground by default, off detached; use off to disable' )
334 . option ( '--events-file <path>' , 'write normalized JSONL events to this file' )
335 . option ( '--vendor-events <path>' , 'write raw vendor events to this diagnostic file' )
336 . option ( '--wall-timeout <duration>' , 'whole-run deadline; use off to disable' )
337 . option ( '--idle-timeout <duration>' , 'maximum time without observed progress' )
338 . option ( '--worker-startup-timeout <duration>' , 'maximum time a vendor sub-worker may show no activity before one retry' )
339 . option ( '--max-agent-turns <count>' , 'maximum agent turns in this supervisor run' )
340 . option ( '--max-no-progress-continuations <count>' , 'maximum same-route retries' )
341 . option ( '--detach' , 'start the supervisor in the background' )
342 . option ( '--no-resume-agent-session' , 'do not reuse an adapter session when supported' );
343 if (intake) {
344 command
345 . option ( '--source-url <https-url>' , 'source URL; credentials are rejected' )
346 . addOption ( new Option ( '--source-mode <mode>' , 'public_content reads public pages; authenticated_api uses normal source API access; private_data needs protected credentials; files_only imports supplied files.' ). choices ([ 'public_content' , 'authenticated_api' , 'private_data' , 'files_only' ]))
347 . option ( '--file <path>' , 'input file for files_only mode; repeat for each source file' , ( value , previous ) => [ ... (previous || []), value])
348 . addOption ( new Option ( '--delivery-mode <mode>' , 'management migrates backend data; website migrates the frontend only; management_and_website performs both.' ). choices ([ 'management' , 'website' , 'management_and_website' ]))
349 . option ( '--destination <strategy>' , 'new creates a site; existing:<site-id> adopts the named existing site' )
350 . addOption ( new Option ( '--site-type <type>' , 'type of a new destination site: headless (website in code), studio or harmony (website designed in a Wix editor). Required before a new site is created; never defaulted.' ). choices ([ 'headless' , 'studio' , 'harmony' ]))
351 . addOption ( new Option ( '--site-business-type <type>' , 'with --site-type harmony only: store, bookings or other records the business context for the run. It does not change how the site is created: Harmony always starts from the blank template.' ). choices ([ 'store' , 'bookings' , 'other' ]))
352 . addOption ( new Option ( '--notification-mute <answer>' , 'with --destination existing:<site-id> only: on mutes all site notifications (including customer emails) until someone unmutes; off leaves them on, so customers and the owner may get emails for imported records. Asked before the first write when not given; never defaulted.' ). choices ([ 'on' , 'off' ]))
353 . addOption ( new Option ( '--management-import-mode <mode>' , 'standard uses the full discovery/mapping flow; quick uses a public adapter and bounded plan.' ). choices ([ 'standard' , 'quick' ]))
354 . option ( '--one-click' , 'record an explicit user one-click decision' )
355 . option ( '--dry-run' , 'request the existing dry-run execution path' );
356 }
357 return command;
358 }
359
360 async function cancelCommand ( args ) {
361 const target = await resolveProject ( projectArg (args));
362 const gracePeriodMs = duration (args.gracePeriod || '60s' , { min: 5000 , max: 600000 });
363 const result = await requestCancellation (target.projectDir, { reason: args.reason || 'canceled by operator' , gracePeriodMs });
364 if (args.wait !== false ) {
365 const deadline = Date. now () + gracePeriodMs + 5000 ;
366 while (Date. now () < deadline && await readActive (target.projectDir)) await new Promise (( resolve ) => setTimeout (resolve, 250 ));
367 const record = await readJson (path. join (target.projectDir, 'state' , 'runner' , 'runs' , `${ result . active . runId }.json` ));
368 if (record?.result) { output (record.result, args.format || 'text' , `${ record . result . outcome }: cancellation finished.` ); return exitCode (record.result); }
369 }
370 output (result, args.format || 'text' , `Cancellation requested for ${ result . active . runId }.` );
371 return 0 ;
372 }
373
374 function setExit ( handler ) {
375 return async ( ... values ) => { process.exitCode = await handler ( ... values); };
376 }
377
378 function createProgram () {
379 const program = new Command ();
380 program
381 . name ( 'replatform' )
382 . description ( 'Non-interactive supervisor for artifact-first RePlatform migrations.' )
383 . version ( VERSION , '--version' , 'print the CLI version' )
384 . showHelpAfterError ( '(all commands are non-interactive; they never prompt, open a pager, or wait for stdin)' )
385 . addHelpText ( 'after' , ' \n Purpose: the CLI supervises an agent using wix-replatform; it does not replace the skill or migration receipts. \n Full guide: CLI.md alongside this installed wix-replatform bundle. \n Exit codes: 0 completed, 1 failed, 2 invalid input, 3 waiting for user, 4 blocked, 5 canceled/interrupted, 6 timeout. \n ' );
386
387 addRuntimeOptions (program. command ( 'run [project]' ). description ( 'Start a non-interactive foreground supervised migration run.' ). option ( '--project <path>' , 'project directory' ), { intake: true })
388 . action ( setExit (( project , options ) => runCommand ( commandArgs ( 'run' , project, options))));
389 addRuntimeOptions (program. command ( 'resume [project]' ). description ( 'Resume an unfinalized migration project non-interactively.' ). option ( '--project <path>' , 'project directory' ), { intake: false })
390 . action ( setExit (( project , options ) => runCommand ( commandArgs ( 'resume' , project, options), { resume: true })));
391 program. command ( 'status [project-or-run-id]' ). description ( 'Read durable orchestration and runner state.' ). option ( '--project <path>' , 'project directory' ). addOption ( new Option ( '--format <format>' , 'stdout format' ). choices ([ 'text' , 'json' ]). default ( 'text' ))
392 . action ( setExit (( project , options ) => statusCommand ( commandArgs ( 'status' , project, options))));
393 program. command ( 'tail [project-or-run-id]' ). description ( 'Render normalized runner events without a pager.' ). option ( '--project <path>' , 'project directory' ). option ( '--follow' , 'wait for events until the run terminates' ). option ( '--since <event-id-or-duration>' , 'exclusive event cursor or inclusive duration window' ). addOption ( new Option ( '--format <format>' , 'stdout format' ). choices ([ 'jsonl' , 'text' ]). default ( 'jsonl' ))
394 . action ( setExit (( project , options ) => tailCommand ( commandArgs ( 'tail' , project, options))));
395 program. command ( 'cancel <project-or-run-id>' ). description ( 'Request supervised cancellation of a live run.' ). option ( '--project <path>' , 'project directory' ). option ( '--grace-period <duration>' , 'grace period before forced termination' , '60s' ). option ( '--no-wait' , 'return after durable acknowledgement' ). option ( '--reason <text>' , 'operator-visible reason' , 'canceled by operator' ). addOption ( new Option ( '--format <format>' , 'stdout format' ). choices ([ 'text' , 'json' ]). default ( 'text' ))
396 . action ( setExit (( project , options ) => cancelCommand ( commandArgs ( 'cancel' , project, options))));
397 program. command ( 'doctor [project-or-run-id]' ). description ( 'Check local agent and project-run capability without exposing credentials.' ). option ( '--project <path>' , 'project directory' ). addOption ( new Option ( '--agent <id>' , 'agent adapter' ). choices ([ 'auto' , 'codex' , 'claude' ]). default ( 'auto' )). option ( '--agent-path <path>' , 'adapter executable override' ). addOption ( new Option ( '--format <format>' , 'stdout format' ). choices ([ 'text' , 'json' ]). default ( 'text' ))
398 . action ( setExit (( project , options ) => doctorCommand ( commandArgs ( 'doctor' , project, options))));
399 program. command ( 'requests [project-or-run-id]' ). description ( 'List durable human actions.' ). option ( '--project <path>' , 'project directory' ). addOption ( new Option ( '--status <status>' , 'request status' ). choices ([ 'pending' , 'answered' , 'canceled' ]). default ( 'pending' )). option ( '--since <duration>' , 'only actions since this duration' ). addOption ( new Option ( '--format <format>' , 'stdout format' ). choices ([ 'text' , 'json' ]). default ( 'text' ))
400 . action ( setExit (( project , options ) => requestsCommand ( commandArgs ( 'requests' , project, options))));
401 program. command ( 'tasks [project-or-run-id]' ). description ( 'Read persisted operator-facing work state.' ). option ( '--project <path>' , 'project directory' ). addOption ( new Option ( '--format <format>' , 'stdout format' ). choices ([ 'text' , 'json' ]). default ( 'text' ))
402 . action ( setExit (( project , options ) => tasksCommand ( commandArgs ( 'tasks' , project, options))));
403 program. command ( 'show [project-or-run-id] [request-id-or-artifact]' ). description ( 'Print a durable action or authorized review artifact. With --project, supply the item positionally.' ). option ( '--project <path>' , 'project directory' ). option ( '--max-bytes <count>' , 'maximum emitted bytes' , '1048576' ). option ( '--cursor <byte-offset>' , 'artifact byte offset' , '0' ). addOption ( new Option ( '--format <format>' , 'stdout format' ). choices ([ 'text' , 'json' ]). default ( 'text' ))
404 . action ( setExit (( first , second , options ) => {
405 const item = second || (options.project ? first : null );
406 const project = options.project || (second ? first : null );
407 if ( ! item) throw new Error ( 'show requires request-id-or-artifact' );
408 return showCommand ({ ... options, project, _: [ 'show' , project, item] });
409 }));
410 addRuntimeOptions (program. command ( 'answer [project-or-run-id] [request-id]' ). description ( 'Record one immutable human response. With --project, supply the request ID positionally or through --request-id.' ). option ( '--project <path>' , 'project directory' ). option ( '--request-id <id>' , 'durable user-action request ID' ). option ( '--choice <choice-id>' , 'declared choice ID' ). option ( '--note <text>' , 'optional reviewer or acknowledgement note' ). option ( '--text <text>' , 'free-text answer' ). option ( '--text-file <path>' , 'UTF-8 answer file' ). option ( '--acknowledge' , 'state that a required manual action was completed; the resumed stage must verify it' ). option ( '--continue' , 'continue in the foreground after answering; preserve explicit run holds' ), { intake: false })
411 . action ( setExit (( first , second , options ) => {
412 const requestId = options.requestId || second || (options.project ? first : null );
413 const project = options.project || (second ? first : null );
414 if ( ! requestId) throw new Error ( 'answer requires request-id (positional or --request-id)' );
415 return answerCommand ({ ... options, project, _: [ 'answer' , project, requestId] });
416 }));
417 program. command ( 'projects' ). description ( 'List migration projects beneath the configured migrations root.' ). addOption ( new Option ( '--format <format>' , 'stdout format' ). choices ([ 'text' , 'json' ]). default ( 'text' ))
418 . action ( setExit (( options ) => projectsCommand (options)));
419 program. command ( 'account' ). description ( 'Report the identity currently authenticated in the local Wix CLI.' ). addOption ( new Option ( '--format <format>' , 'stdout format' ). choices ([ 'text' , 'json' ]). default ( 'text' ))
420 . action ( setExit (( options ) => accountCommand (options)));
421 program. command ( 'completion <shell>' ). description ( 'Print static completion for bash, zsh, or fish.' )
422 . action (( shell ) => {
423 if ( ! [ 'bash' , 'zsh' , 'fish' ]. includes (shell)) throw new Error ( 'completion requires bash, zsh, or fish' );
424 const commands = 'run resume status tail cancel doctor requests tasks show answer projects account help completion' ;
425 if (shell === 'bash' ) process.stdout. write ( `_replatform() { COMPREPLY=( $(compgen -W '${ commands }' -- "${'${COMP_WORDS[1]}'}") ); } \n complete -F _replatform replatform \n ` );
426 if (shell === 'zsh' ) process.stdout. write ( `#compdef replatform \n _arguments '1:command:(${ commands })' '*::arg:->args' \n ` );
427 if (shell === 'fish' ) process.stdout. write ( `${ commands . split ( ' ' ). map (( item ) => `complete -c replatform -f -a ${ item }` ). join ( ' \n ' ) } \n ` );
428 });
429 program. command ( '__supervise <payload>' , { hidden: true }). action ( setExit ( async ( payload ) => {
430 const data = JSON . parse (Buffer. from (payload, 'base64url' ). toString ( 'utf8' ));
431 const controller = new RunController (data.project, { ... data.options, onEvent : () => {} });
432 return exitCode ( await controller. run ());
433 }));
434 return program;
435 }
436
437 async function main ( argv = process.argv. slice ( 2 )) {
438 const program = createProgram ();
439 // Commander remains the single formatter. JSON wraps its generated help for callers
440 // that need discovery without parsing terminal columns.
441 if (argv[ 0 ] === 'help' && argv. includes ( '--format' )) {
442 const format = argv[argv. indexOf ( '--format' ) + 1 ];
443 if ( ! [ 'text' , 'json' ]. includes (format)) throw new Error ( 'help format must be text or json' );
444 const requested = argv. slice ( 1 ). find (( item ) => ! item. startsWith ( '-' ));
445 const target = requested ? program.commands. find (( command ) => command. name () === requested) : program;
446 if (requested && ! target) throw new Error ( `unknown command: ${ requested }` );
447 const helpText = target. helpInformation ();
448 output ({ schemaVersion: 1 , command: requested || null , help: helpText }, format, helpText);
449 return 0 ;
450 }
451 await program. parseAsync ([ 'node' , 'replatform' , ... argv]);
452 return process.exitCode || 0 ;
453 }
454
455 if (require.main === module ) main (). catch (( error ) => { process.stderr. write ( `${ error . message } \n ` ); process.exitCode = 2 ; });
456 module . exports = { main, createProgram, duration, runtimeOptions, resolveProject, allowedArtifact, accountCommand, tasksCommand };