@netlify/agent-runner-cli 1.119.10-alpha.0 → 1.119.12-alpha.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/bin-local.js +103 -105
- package/dist/bin.js +102 -104
- package/dist/index.js +102 -104
- package/dist/scripts/scaffold.js +2 -2
- package/package.json +3 -1
- package/scripts/scenarios/install-skills.mjs +57 -0
- package/scripts/scenarios/netlify-deploy.mjs +311 -0
- package/scripts/scenarios/prewarm-template-mirror.mjs +69 -0
package/dist/scripts/scaffold.js
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import{spawnSync as
|
|
2
|
-
`)}var{templateId:
|
|
1
|
+
import{spawnSync as w}from"child_process";import{existsSync as N,readdirSync as j,statSync as x,writeFileSync as k,mkdirSync as _}from"fs";import{createRequire as v}from"module";import n from"path";import t from"process";var A=".axis-scaffold-skip-install",h=v(import.meta.url);function L(){let e=h("@netlify/ts-cli/package.json"),s=n.dirname(h.resolve("@netlify/ts-cli/package.json"));return n.join(s,e.bin)}function O(e){let s=e.slice(2),r=s[0];(!r||r.startsWith("--"))&&(console.error("Usage: scaffold.js <template-id> [--package-manager npm|pnpm|yarn]"),t.exit(1));let o=s.indexOf("--package-manager"),a=o!==-1?s[o+1]??"":"";return{templateId:r,packageManager:a}}var T=new Set(["node_modules",".git",".netlify",".claude",".next","dist","build",".cache"]);function F(e,s=4){let r=[];function o(a,g){if(g>s)return;let m;try{m=j(a).sort()}catch{return}for(let p of m){if(T.has(p))continue;let c=n.join(a,p),y=n.relative(e,c),S=!1;try{S=x(c).isDirectory()}catch{continue}S?(r.push(y+"/"),o(c,g+1)):r.push(y)}}return o(e,0),r.join(`
|
|
2
|
+
`)}var{templateId:d,packageManager:l}=O(t.argv),C=L(),D=t.env.NVM_BIN?`${t.env.NVM_BIN}/node`:"node",u=[C,"--target-dir","./","--no-git","--json","--add-ons",d];l&&u.push("--package-manager",l);var I=t.env.AXIS_WORKSPACE,E=I!==void 0&&N(n.join(I,A));(t.env.NETLIFY_SCAFFOLD_NO_INSTALL==="1"||E)&&u.push("--no-install");var i=w(D,u,{stdio:["inherit","pipe","inherit"],encoding:"utf8"});i.status!==0&&(i.stdout&&t.stdout.write(i.stdout),t.exit(i.status??1));var f;try{f=JSON.parse(i.stdout)?.starter?.framework}catch{i.stdout&&t.stdout.write(i.stdout)}var M={template:d,packageManager:l};try{_(n.join(t.cwd(),".netlify"),{recursive:!0}),k(n.join(t.cwd(),".netlify","scaffold-result.json"),JSON.stringify(M));let e=F(t.cwd());k(n.join(t.cwd(),".netlify","scaffold-tree.md"),e)}catch{}var R=f?` (${f})`:"";console.log(`\u2713 Scaffolded "${d}" template${R}. File tree: .netlify/scaffold-tree.md`);
|
|
3
3
|
//# sourceMappingURL=scaffold.js.map
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@netlify/agent-runner-cli",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "1.119.
|
|
4
|
+
"version": "1.119.12-alpha.0",
|
|
5
5
|
"description": "CLI tool for running Netlify agents",
|
|
6
6
|
"main": "./dist/index.js",
|
|
7
7
|
"types": "./dist/index.d.ts",
|
|
@@ -65,6 +65,7 @@
|
|
|
65
65
|
"@commitlint/config-conventional": "^20.0.0",
|
|
66
66
|
"@eslint/compat": "^2.0.0",
|
|
67
67
|
"@eslint/js": "^9.35.0",
|
|
68
|
+
"@netlify/axis": "^1.11.0",
|
|
68
69
|
"@netlify/eslint-config-node": "^7.0.1",
|
|
69
70
|
"@types/node": "^24.5.0",
|
|
70
71
|
"@typescript-eslint/eslint-plugin": "^8.0.0",
|
|
@@ -74,6 +75,7 @@
|
|
|
74
75
|
"eslint-config-prettier": "^10.1.8",
|
|
75
76
|
"eslint-plugin-n": "^17.0.0",
|
|
76
77
|
"husky": "^9.0.0",
|
|
78
|
+
"jiti": "^2.7.0",
|
|
77
79
|
"patch-package": "^8.0.0",
|
|
78
80
|
"tsup": "^8.5.0",
|
|
79
81
|
"typescript": "^5.0.0",
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Installs the orchestrator's bundled skills into the agent's skill directory
|
|
4
|
+
* by calling `setupAgentSkills` from `src/context.js` — the same function the
|
|
5
|
+
* orchestrator itself uses at runtime. Intended to run as an axis scenario
|
|
6
|
+
* setup action when the scenario wants to A/B with-skills vs without.
|
|
7
|
+
*
|
|
8
|
+
* Uses `jiti` to import the orchestrator source directly so we don't need a
|
|
9
|
+
* pre-built `dist/` (context.js transitively imports several `.ts` modules
|
|
10
|
+
* that plain Node ESM can't resolve).
|
|
11
|
+
*
|
|
12
|
+
* Usage:
|
|
13
|
+
* node install-skills.mjs [--agent claude-code|codex|gemini] [--target-dir <abs>]
|
|
14
|
+
*
|
|
15
|
+
* Omit `--agent` to install for all known agents in one pass.
|
|
16
|
+
*/
|
|
17
|
+
import process from 'node:process'
|
|
18
|
+
import { parseArgs } from 'node:util'
|
|
19
|
+
|
|
20
|
+
import { createJiti } from 'jiti'
|
|
21
|
+
|
|
22
|
+
const jiti = createJiti(import.meta.url, { interopDefault: true })
|
|
23
|
+
const { setupAgentSkills, resetSkillsCache } = await jiti.import('../../src/context.js')
|
|
24
|
+
|
|
25
|
+
// Axis adapter name → orchestrator runner key (matches `AGENT_SKILLS_DIRS` in src/context.js).
|
|
26
|
+
const RUNNER_BY_AGENT = {
|
|
27
|
+
'claude-code': 'claude',
|
|
28
|
+
codex: 'codex',
|
|
29
|
+
gemini: 'gemini',
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const ALL_AGENTS = Object.keys(RUNNER_BY_AGENT)
|
|
33
|
+
|
|
34
|
+
const main = async () => {
|
|
35
|
+
const { values } = parseArgs({
|
|
36
|
+
args: process.argv.slice(2),
|
|
37
|
+
options: {
|
|
38
|
+
agent: { type: 'string' },
|
|
39
|
+
'target-dir': { type: 'string' },
|
|
40
|
+
},
|
|
41
|
+
strict: false,
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
const agents = values.agent ? [values.agent] : ALL_AGENTS
|
|
45
|
+
|
|
46
|
+
for (const agent of agents) {
|
|
47
|
+
const runner = RUNNER_BY_AGENT[agent] ?? agent
|
|
48
|
+
resetSkillsCache()
|
|
49
|
+
const installed = await setupAgentSkills(runner, values['target-dir'] ? { targetDir: values['target-dir'] } : {})
|
|
50
|
+
process.stderr.write(`[install-skills] ${runner}: ${installed.length} skill(s)\n`)
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
main().catch((err) => {
|
|
55
|
+
process.stderr.write(`[install-skills] error: ${err.message ?? err}\n`)
|
|
56
|
+
process.exit(1)
|
|
57
|
+
})
|
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Shared teardown script for axis scenarios.
|
|
4
|
+
*
|
|
5
|
+
* Looks up (or creates) a Netlify site by name, zips the workspace source, and
|
|
6
|
+
* POSTs the zip to `/sites/{site_id}/builds` so Netlify builds and deploys it
|
|
7
|
+
* server-side. Writes a small markdown block to `$AXIS_OUTPUT` so the deploy
|
|
8
|
+
* URL is rendered in the axis report.
|
|
9
|
+
*
|
|
10
|
+
* Quiet by design: prints nothing on success aside from the JSON summary on
|
|
11
|
+
* stdout. Errors include the raw argv to make argument-passing problems
|
|
12
|
+
* obvious without leaving routine runs noisy.
|
|
13
|
+
*
|
|
14
|
+
* Usage:
|
|
15
|
+
* node netlify-deploy.mjs --name axis-<slug> [--dir .] [--account <id-or-slug>] [--message "…"] [--branch axis-preview]
|
|
16
|
+
*
|
|
17
|
+
* Requires: NETLIFY_AUTH_TOKEN env var, and the `zip` binary on PATH.
|
|
18
|
+
*/
|
|
19
|
+
import { execFile } from 'node:child_process'
|
|
20
|
+
import { appendFile, mkdtemp, readFile, stat } from 'node:fs/promises'
|
|
21
|
+
import { tmpdir } from 'node:os'
|
|
22
|
+
import { join } from 'node:path'
|
|
23
|
+
import process from 'node:process'
|
|
24
|
+
import { parseArgs, promisify } from 'node:util'
|
|
25
|
+
|
|
26
|
+
const execFileAsync = promisify(execFile)
|
|
27
|
+
|
|
28
|
+
const NETLIFY_API = 'https://api.netlify.com/api/v1'
|
|
29
|
+
|
|
30
|
+
const IGNORE_PATTERNS = [
|
|
31
|
+
'node_modules*',
|
|
32
|
+
'.git*',
|
|
33
|
+
'.netlify*',
|
|
34
|
+
'.next*',
|
|
35
|
+
'dist*',
|
|
36
|
+
'build*',
|
|
37
|
+
'.nuxt*',
|
|
38
|
+
'.output*',
|
|
39
|
+
'.vercel*',
|
|
40
|
+
'__pycache__*',
|
|
41
|
+
'.venv*',
|
|
42
|
+
'.env',
|
|
43
|
+
'.DS_Store',
|
|
44
|
+
'Thumbs.db',
|
|
45
|
+
'*.log',
|
|
46
|
+
'.nyc_output*',
|
|
47
|
+
'coverage*',
|
|
48
|
+
'.cache*',
|
|
49
|
+
'.tmp*',
|
|
50
|
+
'.temp*',
|
|
51
|
+
]
|
|
52
|
+
|
|
53
|
+
const SITE_NAME_PATTERN = /^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/
|
|
54
|
+
|
|
55
|
+
const rawArgv = process.argv.slice(2)
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* DNS subdomain labels max out at 63 characters. Netlify deploy preview URLs
|
|
59
|
+
* are `https://<branch>--<site>.netlify.app`, so `branch + '--' + site` has
|
|
60
|
+
* to fit within that limit. We leave one byte of margin (62) so we don't
|
|
61
|
+
* trip any stricter Netlify-side checks.
|
|
62
|
+
*/
|
|
63
|
+
const SUBDOMAIN_MAX = 62
|
|
64
|
+
/** Random suffix chars to keep each axis run unique without overwriting previous deploys. */
|
|
65
|
+
const HEX_LEN = 4
|
|
66
|
+
|
|
67
|
+
const slugifyBranch = (value) =>
|
|
68
|
+
String(value)
|
|
69
|
+
.toLowerCase()
|
|
70
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
71
|
+
.replace(/^-+|-+$/g, '')
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Default branch from `AXIS_AGENT`/`AXIS_VARIANT` so each axis combination
|
|
75
|
+
* gets its own preview URL. Branch length is computed against the site name
|
|
76
|
+
* so the full `<branch>--<site>` subdomain stays within
|
|
77
|
+
* {@link SUBDOMAIN_MAX} characters. When the agent+variant identifier is
|
|
78
|
+
* too long to fit, it is truncated from the right (variant first, then
|
|
79
|
+
* agent), keeping enough room for the hex prefix on every run.
|
|
80
|
+
*/
|
|
81
|
+
const envDefaultBranch = (siteName = '') => {
|
|
82
|
+
const { AXIS_AGENT, AXIS_VARIANT } = process.env
|
|
83
|
+
const parts = [AXIS_AGENT, AXIS_VARIANT].filter(Boolean).map(slugifyBranch).filter(Boolean)
|
|
84
|
+
const joinedRaw = parts.length ? parts.join('-') : 'axis-preview'
|
|
85
|
+
|
|
86
|
+
// budget = SUBDOMAIN_MAX − "--" − siteName − hex − "-" between hex and joined
|
|
87
|
+
const hexOverhead = HEX_LEN + 1
|
|
88
|
+
const maxJoinedLen = Math.max(1, SUBDOMAIN_MAX - 2 - siteName.length - hexOverhead)
|
|
89
|
+
const joined = joinedRaw.slice(0, maxJoinedLen).replace(/-+$/, '')
|
|
90
|
+
|
|
91
|
+
const hex = Math.random().toString(16).slice(2, 2 + HEX_LEN)
|
|
92
|
+
return `${hex}-${joined}`
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** Default message describes the axis combination so Netlify deploy lists are scannable. */
|
|
96
|
+
const envDefaultMessage = () => {
|
|
97
|
+
const { AXIS_AGENT, AXIS_MODEL, AXIS_VARIANT } = process.env
|
|
98
|
+
const parts = []
|
|
99
|
+
if (AXIS_AGENT) parts.push(`agent=${AXIS_AGENT}`)
|
|
100
|
+
if (AXIS_MODEL) parts.push(`model=${AXIS_MODEL}`)
|
|
101
|
+
if (AXIS_VARIANT) parts.push(`variant=${AXIS_VARIANT}`)
|
|
102
|
+
return parts.length ? `axis · ${parts.join(' · ')}` : 'axis scenario teardown'
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const readCliOptions = () => {
|
|
106
|
+
const { values } = parseArgs({
|
|
107
|
+
args: rawArgv,
|
|
108
|
+
options: {
|
|
109
|
+
name: { type: 'string' },
|
|
110
|
+
dir: { type: 'string', default: '.' },
|
|
111
|
+
account: { type: 'string' },
|
|
112
|
+
message: { type: 'string' },
|
|
113
|
+
branch: { type: 'string' },
|
|
114
|
+
},
|
|
115
|
+
strict: false,
|
|
116
|
+
})
|
|
117
|
+
return {
|
|
118
|
+
...values,
|
|
119
|
+
message: values.message ?? envDefaultMessage(),
|
|
120
|
+
branch: values.branch ?? envDefaultBranch(typeof values.name === 'string' ? values.name : ''),
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const validateName = (name) => {
|
|
125
|
+
if (typeof name !== 'string' || name.length === 0) {
|
|
126
|
+
throw new Error(`--name must be a non-empty string. Got: ${typeof name} ${JSON.stringify(name)}`)
|
|
127
|
+
}
|
|
128
|
+
if (!SITE_NAME_PATTERN.test(name)) {
|
|
129
|
+
throw new Error(
|
|
130
|
+
`--name "${name}" is not a valid Netlify subdomain. ` +
|
|
131
|
+
`Must be lowercase letters/digits/hyphens, 1-63 chars, no leading or trailing hyphen.`,
|
|
132
|
+
)
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const apiFetch = async (path, init = {}) => {
|
|
137
|
+
const url = path.startsWith('http') ? path : `${NETLIFY_API}${path}`
|
|
138
|
+
const res = await fetch(url, {
|
|
139
|
+
...init,
|
|
140
|
+
headers: {
|
|
141
|
+
Authorization: `Bearer ${process.env.NETLIFY_AUTH_TOKEN}`,
|
|
142
|
+
'Content-Type': 'application/json',
|
|
143
|
+
...(init.headers ?? {}),
|
|
144
|
+
},
|
|
145
|
+
})
|
|
146
|
+
if (!res.ok) {
|
|
147
|
+
const body = await res.text().catch(() => '')
|
|
148
|
+
throw new Error(`${init.method ?? 'GET'} ${path} → ${res.status}: ${body}`)
|
|
149
|
+
}
|
|
150
|
+
if (res.status === 204) return null
|
|
151
|
+
return res.json()
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const NETLIFY_ID_PATTERN = /^[a-f0-9]{24}$/i
|
|
155
|
+
|
|
156
|
+
let cachedAccountSlug
|
|
157
|
+
const resolveAccountSlug = async (idOrSlug) => {
|
|
158
|
+
if (!idOrSlug) return undefined
|
|
159
|
+
if (!NETLIFY_ID_PATTERN.test(idOrSlug)) return idOrSlug
|
|
160
|
+
if (cachedAccountSlug) return cachedAccountSlug
|
|
161
|
+
const accounts = await apiFetch('/accounts')
|
|
162
|
+
if (!Array.isArray(accounts)) throw new Error('GET /accounts returned non-array')
|
|
163
|
+
const match = accounts.find((a) => a.id === idOrSlug)
|
|
164
|
+
if (!match) {
|
|
165
|
+
throw new Error(`Netlify account id ${idOrSlug} is not accessible by this token (NETLIFY_AUTH_TOKEN).`)
|
|
166
|
+
}
|
|
167
|
+
cachedAccountSlug = match.slug
|
|
168
|
+
return cachedAccountSlug
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const findSiteByName = async (name, accountSlug) => {
|
|
172
|
+
const params = new URLSearchParams({ filter: 'all', name })
|
|
173
|
+
if (accountSlug) params.set('account_slug', accountSlug)
|
|
174
|
+
const sites = await apiFetch(`/sites?${params.toString()}`)
|
|
175
|
+
if (!Array.isArray(sites)) return null
|
|
176
|
+
return sites.find((s) => s.name === name) ?? null
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const isSubdomainTakenError = (err) =>
|
|
180
|
+
/\b422\b/.test(String(err?.message ?? '')) && /subdomain.*must be unique/i.test(String(err?.message ?? ''))
|
|
181
|
+
|
|
182
|
+
const createSite = async (name, accountSlug) => {
|
|
183
|
+
const path = accountSlug ? `/${encodeURIComponent(accountSlug)}/sites` : '/sites'
|
|
184
|
+
return apiFetch(path, { method: 'POST', body: JSON.stringify({ name }) })
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const lookupOrCreateSite = async (name, accountSlug) => {
|
|
188
|
+
const existing = await findSiteByName(name, accountSlug)
|
|
189
|
+
if (existing) return existing
|
|
190
|
+
try {
|
|
191
|
+
return await createSite(name, accountSlug)
|
|
192
|
+
} catch (err) {
|
|
193
|
+
if (!isSubdomainTakenError(err)) throw err
|
|
194
|
+
const retry = await findSiteByName(name, accountSlug)
|
|
195
|
+
if (retry) return retry
|
|
196
|
+
throw new Error(
|
|
197
|
+
`Netlify site name '${name}' is taken by another account this token cannot access. ` +
|
|
198
|
+
`Pick a different siteName or grant this token access to the existing site.`,
|
|
199
|
+
)
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
const ensureSourceDir = async (dir) => {
|
|
204
|
+
let info
|
|
205
|
+
try {
|
|
206
|
+
info = await stat(dir)
|
|
207
|
+
} catch (err) {
|
|
208
|
+
throw new Error(`source dir does not exist: ${dir} (${err.code})`)
|
|
209
|
+
}
|
|
210
|
+
if (!info.isDirectory()) throw new Error(`source dir is not a directory: ${dir}`)
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const zipSource = async (sourceDir) => {
|
|
214
|
+
const tmp = await mkdtemp(join(tmpdir(), 'axis-deploy-'))
|
|
215
|
+
const zipPath = join(tmp, 'source.zip')
|
|
216
|
+
const excludeArgs = IGNORE_PATTERNS.flatMap((p) => ['-x', p])
|
|
217
|
+
await execFileAsync('zip', ['-r', '-q', zipPath, '.', ...excludeArgs], {
|
|
218
|
+
cwd: sourceDir,
|
|
219
|
+
maxBuffer: 100 * 1024 * 1024,
|
|
220
|
+
})
|
|
221
|
+
return zipPath
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const triggerBuildFromZip = async ({ siteId, zipPath, title, branch }) => {
|
|
225
|
+
const body = await readFile(zipPath)
|
|
226
|
+
const params = new URLSearchParams({ title })
|
|
227
|
+
if (branch) params.set('branch', branch)
|
|
228
|
+
const url = `${NETLIFY_API}/sites/${siteId}/builds?${params.toString()}`
|
|
229
|
+
const res = await fetch(url, {
|
|
230
|
+
method: 'POST',
|
|
231
|
+
body,
|
|
232
|
+
headers: {
|
|
233
|
+
Authorization: `Bearer ${process.env.NETLIFY_AUTH_TOKEN}`,
|
|
234
|
+
'Content-Type': 'application/zip',
|
|
235
|
+
'Content-Length': String(body.length),
|
|
236
|
+
},
|
|
237
|
+
})
|
|
238
|
+
if (!res.ok) {
|
|
239
|
+
const text = await res.text().catch(() => '')
|
|
240
|
+
throw new Error(`POST /sites/${siteId}/builds → ${res.status}: ${text}`)
|
|
241
|
+
}
|
|
242
|
+
return res.json()
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
const fetchDeploy = async (deployId) => {
|
|
246
|
+
try {
|
|
247
|
+
return await apiFetch(`/deploys/${deployId}`)
|
|
248
|
+
} catch {
|
|
249
|
+
return null
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
const writeAxisOutput = async (markdown) => {
|
|
254
|
+
const path = process.env.AXIS_OUTPUT
|
|
255
|
+
if (!path) return
|
|
256
|
+
await appendFile(path, markdown.endsWith('\n') ? markdown : `${markdown}\n`)
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Markdown body appended to `$AXIS_OUTPUT`. Renders as clickable links in the
|
|
261
|
+
* axis report. Falls back gracefully when fields are still provisioning.
|
|
262
|
+
*/
|
|
263
|
+
const buildReportMarkdown = ({ siteName, site, build, deploy, branch }) => {
|
|
264
|
+
const previewUrl = deploy?.deploy_ssl_url || deploy?.deploy_url
|
|
265
|
+
const branchUrl = branch && site?.ssl_url ? site.ssl_url.replace(/^https:\/\//, `https://${branch}--`) : null
|
|
266
|
+
const liveUrl = previewUrl || branchUrl
|
|
267
|
+
const adminLogsUrl = `${site.admin_url}/deploys/${build.deploy_id}`
|
|
268
|
+
const sitePageUrl = site.admin_url
|
|
269
|
+
|
|
270
|
+
const links = []
|
|
271
|
+
if (liveUrl) links.push(`[Live preview](${liveUrl})`)
|
|
272
|
+
links.push(`[Build logs](${adminLogsUrl})`)
|
|
273
|
+
links.push(`[Site dashboard](${sitePageUrl})`)
|
|
274
|
+
|
|
275
|
+
return `## Netlify deploy — \`${siteName}\`\n\n${links.join(' · ')}\n`
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
const main = async () => {
|
|
279
|
+
const { name, dir, account, message, branch } = readCliOptions()
|
|
280
|
+
validateName(name)
|
|
281
|
+
if (!process.env.NETLIFY_AUTH_TOKEN) throw new Error('NETLIFY_AUTH_TOKEN is not set')
|
|
282
|
+
|
|
283
|
+
await ensureSourceDir(dir)
|
|
284
|
+
const accountSlug = await resolveAccountSlug(account)
|
|
285
|
+
const site = await lookupOrCreateSite(name, accountSlug)
|
|
286
|
+
const zipPath = await zipSource(dir)
|
|
287
|
+
const build = await triggerBuildFromZip({ siteId: site.id, zipPath, title: message, branch })
|
|
288
|
+
const deploy = await fetchDeploy(build.deploy_id)
|
|
289
|
+
|
|
290
|
+
await writeAxisOutput(buildReportMarkdown({ siteName: name, site, build, deploy, branch }))
|
|
291
|
+
|
|
292
|
+
process.stdout.write(
|
|
293
|
+
JSON.stringify({
|
|
294
|
+
siteId: site.id,
|
|
295
|
+
siteName: site.name,
|
|
296
|
+
siteUrl: site.ssl_url || site.url,
|
|
297
|
+
buildId: build.id,
|
|
298
|
+
deployId: build.deploy_id,
|
|
299
|
+
deployUrl: deploy?.deploy_ssl_url || deploy?.deploy_url,
|
|
300
|
+
adminLogsUrl: `${site.admin_url}/deploys/${build.deploy_id}`,
|
|
301
|
+
}) + '\n',
|
|
302
|
+
)
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
main().catch((err) => {
|
|
306
|
+
// Include argv on error so argument-passing bugs (e.g. shells dropping a
|
|
307
|
+
// quoted value) are visible without dumping argv on every successful run.
|
|
308
|
+
process.stderr.write(`[deploy] error: ${err.message ?? err}\n`)
|
|
309
|
+
process.stderr.write(`[deploy] argv: ${JSON.stringify(rawArgv)}\n`)
|
|
310
|
+
process.exit(1)
|
|
311
|
+
})
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Populates a per-scenario local mirror of `netlify/swar-templates` inside the
|
|
4
|
+
* current AXIS workspace, at the path `@netlify/ts-cli` falls back to when its
|
|
5
|
+
* GitHub fetch is blocked by the agent sandbox.
|
|
6
|
+
*
|
|
7
|
+
* The path comes from the same constant the create-mode prompt uses
|
|
8
|
+
* (`TS_CLI_TEMPLATE_MIRROR_FALLBACK_BASE` in `src/stages/create.ts`), passed
|
|
9
|
+
* in via env so production and axis stay in sync without this script having
|
|
10
|
+
* to import TypeScript.
|
|
11
|
+
*
|
|
12
|
+
* Env vars:
|
|
13
|
+
* - `AXIS_WORKSPACE` — set by axis on every lifecycle script; the
|
|
14
|
+
* scenario's workspace dir.
|
|
15
|
+
* - `TS_CLI_TEMPLATE_MIRROR_BASE` — passed by the helper that schedules
|
|
16
|
+
* this script (`scenarioPrewarmTemplateMirror` in
|
|
17
|
+
* `scenarios/helpers/scenario-prompts.ts`); should match the default
|
|
18
|
+
* value emitted in the create-mode scaffold command.
|
|
19
|
+
*
|
|
20
|
+
* ts-cli wipes the mirror via `cleanupLocalMirror()` after each scaffold, but
|
|
21
|
+
* that's fine — axis tears the workspace down at the end of the scenario, so
|
|
22
|
+
* the mirror has no lifetime beyond a single ts-cli run.
|
|
23
|
+
*/
|
|
24
|
+
import { execSync } from 'node:child_process'
|
|
25
|
+
import { existsSync, mkdirSync } from 'node:fs'
|
|
26
|
+
import { dirname, isAbsolute, join, resolve } from 'node:path'
|
|
27
|
+
import process from 'node:process'
|
|
28
|
+
|
|
29
|
+
const REPO = 'https://github.com/netlify/swar-templates.git'
|
|
30
|
+
|
|
31
|
+
const workspace = process.env.AXIS_WORKSPACE
|
|
32
|
+
const mirrorBase = process.env.TS_CLI_TEMPLATE_MIRROR_BASE
|
|
33
|
+
|
|
34
|
+
if (!workspace) {
|
|
35
|
+
process.stderr.write('[prewarm-template-mirror] AXIS_WORKSPACE is not set (run this from an axis setup phase)\n')
|
|
36
|
+
process.exit(1)
|
|
37
|
+
}
|
|
38
|
+
if (!mirrorBase) {
|
|
39
|
+
process.stderr.write(
|
|
40
|
+
'[prewarm-template-mirror] TS_CLI_TEMPLATE_MIRROR_BASE is not set (pass it via the scenario helper)\n',
|
|
41
|
+
)
|
|
42
|
+
process.exit(1)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const absBase = isAbsolute(mirrorBase) ? mirrorBase : resolve(workspace, mirrorBase)
|
|
46
|
+
const mirrorDir = join(absBase, 'cache', 'cwd', 'swar-templates')
|
|
47
|
+
const manifestPath = join(mirrorDir, 'manifest.json')
|
|
48
|
+
|
|
49
|
+
if (existsSync(manifestPath)) {
|
|
50
|
+
process.stderr.write(`[prewarm-template-mirror] mirror already populated at ${mirrorDir}\n`)
|
|
51
|
+
process.exit(0)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
mkdirSync(dirname(mirrorDir), { recursive: true })
|
|
55
|
+
process.stderr.write(`[prewarm-template-mirror] cloning ${REPO} → ${mirrorDir}\n`)
|
|
56
|
+
|
|
57
|
+
try {
|
|
58
|
+
execSync(`git clone --depth 1 "${REPO}" "${mirrorDir}"`, { stdio: 'inherit' })
|
|
59
|
+
} catch (err) {
|
|
60
|
+
process.stderr.write(`[prewarm-template-mirror] git clone failed: ${err.message ?? err}\n`)
|
|
61
|
+
process.exit(1)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (!existsSync(manifestPath)) {
|
|
65
|
+
process.stderr.write(`[prewarm-template-mirror] clone succeeded but ${manifestPath} is missing\n`)
|
|
66
|
+
process.exit(1)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
process.stderr.write(`[prewarm-template-mirror] ready at ${mirrorDir}\n`)
|