Setting the file. One moment.
Fast Path · Wix Headless Fast · wix/skills · Skills Docs
ContentsBack to the top of the page install/fast-path.mjs
install/ fast-path.mjs
JavaScript · 184 lines · 10 KB
14 // reported in the final event), so the caller can build the brand layer while they finish.
15 // Steps: scaffold (Wix CLI; requires a logged-in session) → deploy shipped code + deps +
16 // lockfile → pin AGENTS.md → start `npm ci || npm install` detached → start the seed detached.
17 import { spawn, spawnSync } from "node:child_process" ;
18 import { existsSync, openSync, readFileSync, readdirSync, renameSync, rmSync } from "node:fs" ;
19 import { dirname, join, resolve } from "node:path" ;
20 import { fileURLToPath } from "node:url" ;
21
22 const SKILL_ROOT = resolve ( dirname ( fileURLToPath ( import . meta .url)), ".." );
23
24 const emit = ( event , extra = {}) => console. log ( JSON . stringify ({ event, ... extra }));
25 const fail = ( step , detail ) => {
26 emit ( "error" , { step, detail: String (detail). slice ( 0 , 600 ) });
27 process. exit ( 1 );
28 };
29
30 // ---- args ---------------------------------------------------------------------------------------
31 const argv = process.argv. slice ( 2 );
32 const flag = ( name ) => {
33 const i = argv. indexOf ( `--${ name }` );
34 return i !== - 1 && argv[i + 1 ] && ! argv[i + 1 ]. startsWith ( "--" ) ? argv[i + 1 ] : null ;
35 };
36 const businessName = flag ( "business-name" );
37 const planPath = flag ( "plan" );
38 const vertical = flag ( "vertical" );
39 const stack = flag ( "stack" ) ?? "astro" ;
40 // Opt-in: leave the project in the current directory (flat) instead of a subfolder.
41 const flatten = argv. includes ( "--flatten" );
42 // --vertical is REQUIRED: a defaulted vertical deploys the wrong code and runs the wrong
43 // seed against the plan — fail loudly with the discovered choices instead.
44 const knownVerticals = readdirSync ( join ( SKILL_ROOT , "references" ), { withFileTypes: true })
45 . filter (( d ) => d. isDirectory () && d.name !== "shared" && existsSync ( join ( SKILL_ROOT , "references" , d.name, "app" )))
46 . map (( d ) => d.name);
47 if ( ! businessName || ! planPath || ! vertical) {
48 fail ( "args" , `usage: fast-path.mjs --business-name "<Brand>" --plan plan.json --vertical <${ knownVerticals . join ( "|" ) }> [--stack astro]` );
49 }
50 if ( ! knownVerticals. includes (vertical)) {
51 fail ( "args" , `unknown vertical "${ vertical }" — shipped verticals: ${ knownVerticals . join ( ", " ) }` );
52 }
53 if ( ! existsSync (planPath)) fail ( "args" , `plan file not found: ${ planPath }` );
54 const plan = JSON . parse ( readFileSync (planPath, "utf8" ));
55
56 const folderName =
57 flag ( "folder-name" ) ??
58 businessName. toLowerCase (). replace ( / [ ^ a-z0-9] + / g , "-" ). replace ( / ^ - +| - +$ / g , "" ). slice ( 0 , 40 );
59 // `let`: after deploy we flatten the scaffold up into the repo root and repoint
60 // projectDir there, so the detached install/seed and every reported path use the
61 // final location.
62 let projectDir = resolve (process. cwd (), folderName);
63
64 // ---- 1 · scaffold -------------------------------------------------------------------------------
65 if ( existsSync ( join (process. cwd (), "wix.config.json" ))) {
66 fail ( "scaffold" , "the current directory is already a Wix project — this script is for CREATE runs only; use deploy.mjs + the seed module directly" );
67 }
68 if ( existsSync ( join (projectDir, "wix.config.json" ))) {
69 emit ( "scaffold_skipped" , { reason: "project already exists" , folder: folderName });
70 } else {
71 emit ( "scaffolding" , { folder: folderName });
72 const scaffold = spawnSync (
73 "npm" ,
74 // --skip-git: this wrapper composes its own steps and leaves version control to
75 // the caller / the enclosing repo; the scaffold's own `git init` + "Initial
76 // commit" is noise here, and becomes a nested-repo (submodule gitlink) hazard
77 // if the project is later placed inside an existing repo. --skip-install for the
78 // same reason: deps install in a detached step below.
79 [ "create" , "@wix/new@latest" , "--" , "headless" ,
80 "--folder-name" , folderName, "--business-name" , businessName,
81 "--site-template" , "--skip-install" , "--skip-git" , "--no-publish" ],
82 { env: { ... process.env, CI: "1" }, encoding: "utf8" , timeout: 300_000 },
83 );
84 if (scaffold.status !== 0 || ! existsSync ( join (projectDir, "wix.config.json" ))) {
85 fail ( "scaffold" , (scaffold.stderr || scaffold.stdout || "scaffold produced no wix.config.json — is the Wix CLI logged in? (npx @wix/cli@latest whoami)" ). slice ( - 600 ));
86 }
87 }
88 const wixConfig = JSON . parse ( readFileSync ( join (projectDir, "wix.config.json" ), "utf8" ));
89 const siteId = wixConfig.siteId ?? wixConfig.projectId;
90 emit ( "scaffolded" , { folder: folderName, siteId });
91
92 // ---- 2 · deploy shipped code + deps + lockfile ---------------------------------------------------
93 const deploy = spawnSync (
94 "node" ,
95 [
96 join ( SKILL_ROOT , "install" , "deploy.mjs" ),
97 vertical,
98 "--stack" ,
99 stack,
100 "--plan" ,
101 resolve (planPath),
102 ],
103 { cwd: projectDir, encoding: "utf8" , timeout: 60_000 },
104 );
105 if (deploy.status !== 0 ) fail ( "deploy" , deploy.stderr || deploy.stdout);
106 let deployResult = {};
107 try { deployResult = JSON . parse (deploy.stdout); } catch { /* keep going with raw output below */ }
108 if (deployResult.error) fail ( "deploy" , deployResult.error);
109 emit ( "deployed" , deployResult);
110
111 // ---- 2b · record what this project is, for later sessions ---------------------------------------
112 const pin = spawnSync (
113 "node" ,
114 [ join ( SKILL_ROOT , "install" , "pin-agents-md.mjs" ), "--vertical" , vertical, "--stack" , stack],
115 { cwd: projectDir, encoding: "utf8" , timeout: 10_000 },
116 );
117 try { emit ( "agents_md" , JSON . parse (pin.stdout)); } catch { /* never block the build on the note */ }
118
119 // ---- 2c · optional --flatten: move the scaffold into the current directory ----------------------
120 // OFF by default — the normal output is a self-contained subfolder, unchanged. Opt in when the
121 // caller wants the project directly in the current directory (e.g. bootstrapping into an existing
122 // repo that must stay one flat tree). Done HERE, before the detached install below, on purpose:
123 // no node_modules exists yet, so the move is instant and cannot collide with a running install —
124 // the failure mode when a project is flattened by hand after the background install has started.
125 // A pure move: the scaffold was created with --skip-git (above), so there is no nested repo to
126 // reconcile — git is whatever the destination already is.
127 const targetDir = process. cwd ();
128 if (flatten && projectDir !== targetDir) {
129 try {
130 for ( const entry of readdirSync (projectDir)) {
131 renameSync ( join (projectDir, entry), join (targetDir, entry));
132 }
133 rmSync (projectDir, { recursive: true , force: true });
134 projectDir = targetDir;
135 emit ( "flattened" , { into: targetDir });
136 } catch (e) {
137 fail ( "flatten" , e?.stack || e);
138 }
139 }
140
141 // ---- 3 · start the dependency install, detached --------------------------------------------------
142 const installLog = join (projectDir, "npm-install.log" );
143 const logFd = openSync (installLog, "a" );
144 const install = spawn ( "sh" , [ "-c" , "npm ci --ignore-scripts || npm install --ignore-scripts" ], {
145 cwd: projectDir,
146 detached: true ,
147 stdio: [ "ignore" , logFd, logFd],
148 });
149 install. unref ();
150 emit ( "install_started" , { log: installLog, doneMarker: "node_modules/.package-lock.json" });
151
152 // ---- 4 · start the seed, detached ----------------------------------------------------------------
153 // The seed includes a Wix-side provisioning wait of unpredictable length (10-80s); running it
154 // in the caller's foreground would idle the agent for exactly that long. Detach it like the
155 // install: result JSON + exit-code marker land as files the caller syncs on before release.
156 const seedDir = join ( SKILL_ROOT , "references" , vertical, "seed" );
157 const seedName = existsSync (seedDir)
158 ? readdirSync (seedDir). find (( f ) => f. startsWith ( "seed-" ) && f. endsWith ( ".mjs" ))
159 : undefined ;
160 const seedFile = seedName ? join (seedDir, seedName) : null ;
161 if ( ! seedFile) fail ( "seed" , `no seed module found under ${ seedDir }` );
162 const seedResultFile = join (projectDir, "seed-result.json" );
163 const seedLog = join (projectDir, "seed.log" );
164 const seedDoneMarker = join (projectDir, ".seed-exit" );
165 const planAbs = resolve (planPath);
166 const seedChild = spawn (
167 "sh" ,
168 [ "-c" , `node "${ seedFile }" "${ planAbs }" > seed-result.json 2> seed.log; echo $? > .seed-exit` ],
169 { cwd: projectDir, detached: true , stdio: "ignore" },
170 );
171 seedChild. unref ();
172 emit ( "seeding_started" , { vertical, resultFile: seedResultFile, log: seedLog, doneMarker: seedDoneMarker });
173
174 // ---- done ----------------------------------------------------------------------------------------
175 emit ( "ready_for_brand_layer" , {
176 projectDir,
177 siteId,
178 dashboardUrl: deployResult.dashboardUrl,
179 productsUrl: deployResult.productsUrl,
180 categoriesUrl: deployResult.categoriesUrl,
181 install: { log: installLog, doneMarker: "node_modules/.package-lock.json" },
182 seed: { resultFile: "seed-result.json" , log: "seed.log" , doneMarker: ".seed-exit" , success: "file contains 0" },
183 next: "theme SiteLayout + write the home page; then wait for BOTH done markers, verify .seed-exit is 0 (else read seed.log and re-run the seed module), build, release" ,
184 });