@uknows/setup 0.1.0 → 0.2.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.
Files changed (48) hide show
  1. package/README.md +99 -32
  2. package/lib/automate.mjs +1299 -0
  3. package/lib/check.mjs +149 -33
  4. package/lib/ci.mjs +21 -0
  5. package/lib/cli.mjs +114 -29
  6. package/lib/cloudid.mjs +456 -0
  7. package/lib/code.mjs +147 -0
  8. package/lib/context.mjs +126 -0
  9. package/lib/contract-docs.mjs +1780 -198
  10. package/lib/contract.mjs +93 -10
  11. package/lib/detect.mjs +96 -44
  12. package/lib/dockerfile.mjs +83 -0
  13. package/lib/endpoint.mjs +36 -5
  14. package/lib/engine.mjs +1272 -126
  15. package/lib/existing.mjs +448 -54
  16. package/lib/git.mjs +53 -0
  17. package/lib/hooks/UknowsOpenTelemetry.cs +118 -0
  18. package/lib/hooks/pins.mjs +28 -0
  19. package/lib/hooks/uknows-otel.cjs +82 -0
  20. package/lib/hooks/uknows_otel.py +179 -0
  21. package/lib/hosts.mjs +521 -16
  22. package/lib/locks.mjs +353 -0
  23. package/lib/manifest.mjs +27 -7
  24. package/lib/members.mjs +269 -0
  25. package/lib/plan.mjs +130 -0
  26. package/lib/settings.mjs +216 -28
  27. package/lib/signin.mjs +259 -0
  28. package/lib/stacks/azure.mjs +345 -0
  29. package/lib/stacks/channel.mjs +125 -0
  30. package/lib/stacks/dotnet.mjs +336 -29
  31. package/lib/stacks/functions-hooks.mjs +228 -0
  32. package/lib/stacks/functions.mjs +148 -0
  33. package/lib/stacks/guided.mjs +153 -30
  34. package/lib/stacks/index.mjs +10 -2
  35. package/lib/stacks/java.mjs +14 -13
  36. package/lib/stacks/node.mjs +142 -19
  37. package/lib/stacks/otel.mjs +56 -4
  38. package/lib/stacks/python-bootstrap.mjs +27 -7
  39. package/lib/stacks/python-lock.mjs +105 -0
  40. package/lib/stacks/python-sets.mjs +143 -0
  41. package/lib/stacks/python.mjs +261 -63
  42. package/lib/toml.mjs +64 -0
  43. package/lib/types.mjs +52 -5
  44. package/lib/ui.mjs +139 -20
  45. package/lib/updates.mjs +398 -0
  46. package/lib/versions.mjs +115 -0
  47. package/lib/workspace.mjs +4 -1
  48. package/package.json +3 -8
package/lib/locks.mjs ADDED
@@ -0,0 +1,353 @@
1
+ /*
2
+ * The packages that 1 service installs, read from its lockfiles: name -> {version, via, file, line}. It reads,
3
+ * in this order: uv.lock, poetry.lock, pinned requirements files, packages.lock.json (every target framework),
4
+ * obj/project.assets.json, and package-lock.json v2 and v3. It reads text only and runs nothing.
5
+ *
6
+ * direct: the service or 1 of its first-party members lists the package. via: for the other packages, the direct
7
+ * dependencies they arrive through. requires: what the package needs, with the version limit when the lock has it.
8
+ */
9
+ import { posix } from 'node:path'
10
+ import { upFrom } from './workspace.mjs'
11
+ import { pythonProjectName } from './members.mjs'
12
+ import { pyName, tomlArray, tomlEntries, tomlInline, tomlTable } from './toml.mjs'
13
+
14
+ /**
15
+ * @typedef {Object} Installed
16
+ * @property {string} name As the lockfile writes it.
17
+ * @property {string|null} version
18
+ * @property {string} file The lockfile.
19
+ * @property {number} [line] 1-based line of its entry.
20
+ * @property {boolean} direct
21
+ * @property {string[]} via Keys of the direct dependencies it arrives through. Empty when it is direct.
22
+ * @property {{name: string, spec: string|null, line?: number}[]} requires
23
+ * @property {string|null} source registry, editable, virtual, directory, path, git, url, project, link or file. null when unknown.
24
+ */
25
+ /** @typedef {{packages: Map<string, Installed>, files: string[]}} InstalledSet Keys are packageKey() names. */
26
+
27
+ const MAX_LOCK = 16_000_000
28
+ const MAX_VISITS = 50_000
29
+ const join = (dir, file) => (dir === '.' ? file : `${dir}/${file}`)
30
+ const dirOf = (path) => posix.dirname(path)
31
+ const read = (view, path) => {
32
+ try { return (view.stat?.(path)?.size ?? 0) > MAX_LOCK ? null : view.read(path) } catch { return null }
33
+ }
34
+ const json = (text) => { try { const v = JSON.parse(text ?? ''); return v && typeof v === 'object' && !Array.isArray(v) ? v : null } catch { return null } }
35
+ const entry = (name, version, file, line, source = null) => ({ name, version, file, line, direct: false, via: [], requires: [], source })
36
+
37
+ /** The key of a package name: PEP 503 for Python, lower case for NuGet, exact for npm. */
38
+ export const packageKey = (lang, name) => (lang === 'python' ? pyName(name) : lang === 'dotnet' ? String(name).toLowerCase() : String(name))
39
+
40
+ /** An empty set, for services that have no lockfile. */
41
+ export const noPackages = () => ({ packages: new Map(), files: [] })
42
+
43
+ /**
44
+ * @param {import('./types.mjs').Service} service
45
+ * @param {import('./workspace.mjs').RepoView} view
46
+ * @param {import('./members.mjs').Member[]} [members]
47
+ * @returns {InstalledSet}
48
+ */
49
+ export function installedPackages(service, view, members = []) {
50
+ const out = noPackages()
51
+ const merge = (set) => {
52
+ if (!set) return
53
+ out.files.push(set.file)
54
+ for (const [key, item] of set.packages) if (!out.packages.has(key)) out.packages.set(key, item)
55
+ }
56
+ const dirs = [service.dir, ...members.map((m) => m.dir)]
57
+ if (service.lang === 'python') {
58
+ const roots = [pythonProjectName(view, service.dir), ...members.map((m) => m.name && pyName(m.name))].filter(Boolean)
59
+ const uv = nearest(view, service.dir, 'uv.lock')
60
+ if (uv) merge(uvLock(uv, read(view, uv), roots))
61
+ const poetry = nearest(view, service.dir, 'poetry.lock')
62
+ if (poetry) merge(poetryLock(poetry, read(view, poetry), pythonDirectNames(view, dirs)))
63
+ for (const dir of dirs) {
64
+ for (const name of list(view, dir).filter((n) => /^requirements[\w.-]*\.txt$/.test(n))) merge(requirementPins(join(dir, name), read(view, join(dir, name))))
65
+ }
66
+ } else if (service.lang === 'dotnet') {
67
+ for (const csproj of [...service.manifests, ...members.flatMap((m) => m.manifests)].filter((m) => m.endsWith('.csproj'))) {
68
+ const lock = join(dirOf(csproj), 'packages.lock.json')
69
+ merge(nugetLock(lock, read(view, lock)))
70
+ const assets = join(dirOf(csproj), 'obj/project.assets.json')
71
+ merge(nugetAssets(assets, read(view, assets)))
72
+ }
73
+ } else if (service.lang === 'node') {
74
+ const lock = nearest(view, service.dir, 'package-lock.json') ?? nearest(view, service.dir, 'npm-shrinkwrap.json')
75
+ if (lock) {
76
+ const base = dirOf(lock)
77
+ const rel = (dir) => (dir === base ? '' : base === '.' ? (dir === '.' ? '' : dir) : posix.relative(base, dir))
78
+ merge(npmLock(lock, read(view, lock), [rel(service.dir), ...members.map((m) => rel(m.dir))]))
79
+ }
80
+ }
81
+ return out
82
+ }
83
+
84
+ /** The nearest file with this name, from dir up to the top of the git repo. */
85
+ export function nearest(view, dir, name) {
86
+ for (const d of upFrom(dir, view.top ?? '.')) if (read(view, join(d, name)) !== null) return join(d, name)
87
+ return null
88
+ }
89
+
90
+ function list(view, dir) {
91
+ try { return view.list ? view.list(dir) : [] } catch { return [] }
92
+ }
93
+
94
+ /**
95
+ * Set direct and via from a graph. graph: key -> dependency keys. firstParty: the service and its members. Their
96
+ * dependencies are direct, and the walk goes through them without a via.
97
+ */
98
+ function walk(packages, graph, firstParty) {
99
+ const queue = [...firstParty].filter((k) => graph.has(k)).map((k) => [k, null])
100
+ const seen = new Set()
101
+ for (let visits = 0; queue.length && visits < MAX_VISITS; visits++) {
102
+ const [key, via] = queue.shift()
103
+ const tag = `${key}\u0000${via ?? ''}`
104
+ if (seen.has(tag)) continue
105
+ seen.add(tag)
106
+ for (const dep of graph.get(key) ?? []) {
107
+ const item = packages.get(dep)
108
+ if (!item) continue
109
+ if (firstParty.has(dep)) { queue.push([dep, null]); continue }
110
+ if (via === null) item.direct = true
111
+ else if (!item.via.includes(via)) item.via.push(via)
112
+ queue.push([dep, via ?? dep])
113
+ }
114
+ }
115
+ for (const item of packages.values()) if (item.direct) item.via = []
116
+ }
117
+
118
+ // --- Python ----------------------------------------------------------------------------
119
+
120
+ /** uv.lock: the [[package]] blocks, their dependencies, and [package.metadata] requires-dist (local packages have it). */
121
+ export function uvLock(file, text, roots) {
122
+ if (typeof text !== 'string') return null
123
+ const blocks = []
124
+ let b = null, section = '', array = null
125
+ const items = (line, i) => [...line.matchAll(/\{\s*name\s*=\s*"([^"]+)"([^}]*)\}/g)].map((m) => ({ name: m[1], rest: m[2], line: i + 1 }))
126
+ text.split(/\r?\n/).forEach((raw, i) => {
127
+ const line = raw.trim()
128
+ if (line === '[[package]]') { b = { name: null, version: null, source: null, deps: [], requires: [], line: i + 1 }; blocks.push(b); section = 'package'; array = null; return }
129
+ if (/^\[\[/.test(line)) { section = line; array = null; return }
130
+ const header = /^\[([^\]]+)\]$/.exec(line)
131
+ if (header) { section = header[1]; array = null; return }
132
+ if (!b || !section.startsWith('package')) return
133
+ if (array) {
134
+ array.push(...items(line, i))
135
+ if (/^\]/.test(line)) array = null
136
+ return
137
+ }
138
+ const kv = /^([A-Za-z0-9_-]+)\s*=\s*(.*)$/.exec(line)
139
+ if (!kv) return
140
+ const open = (target) => { target.push(...items(kv[2], i)); if (/^\[/.test(kv[2]) && !/\]\s*$/.test(kv[2])) array = target }
141
+ if (section === 'package') {
142
+ if (kv[1] === 'name') { b.name = /^"([^"]+)"/.exec(kv[2])?.[1] ?? null; b.line = i + 1 }
143
+ else if (kv[1] === 'version') b.version = /^"([^"]+)"/.exec(kv[2])?.[1] ?? null
144
+ else if (kv[1] === 'source') b.source = Object.keys(tomlInline(kv[2]) ?? {})[0] ?? null
145
+ else if (kv[1] === 'dependencies') open(b.deps)
146
+ } else if (section === 'package.metadata' && kv[1] === 'requires-dist') open(b.requires)
147
+ })
148
+ const packages = new Map(), graph = new Map()
149
+ for (const block of blocks.filter((x) => x.name)) {
150
+ const key = pyName(block.name)
151
+ if (packages.has(key)) continue
152
+ const item = entry(block.name, block.version, file, block.line, block.source)
153
+ const specs = new Map(block.requires.map((r) => [pyName(r.name), { spec: /specifier\s*=\s*"([^"]*)"/.exec(r.rest)?.[1] ?? null, line: r.line }]))
154
+ const deps = [...new Set(block.deps.map((d) => pyName(d.name)))]
155
+ item.requires = deps.map((k) => ({ name: k, spec: specs.get(k)?.spec ?? null, line: specs.get(k)?.line ?? block.deps.find((d) => pyName(d.name) === k)?.line }))
156
+ for (const [k, s] of specs) if (!deps.includes(k)) item.requires.push({ name: k, spec: s.spec, line: s.line })
157
+ packages.set(key, item)
158
+ graph.set(key, deps)
159
+ }
160
+ const local = new Set(['editable', 'virtual', 'directory'])
161
+ walk(packages, graph, new Set(roots.filter((k) => local.has(packages.get(k)?.source ?? ''))))
162
+ return { file, packages }
163
+ }
164
+
165
+ /** poetry.lock: [[package]] blocks and their [package.dependencies]. Optional dependencies are left out. */
166
+ export function poetryLock(file, text, direct) {
167
+ if (typeof text !== 'string') return null
168
+ const packages = new Map(), graph = new Map()
169
+ const lines = text.split(/\r?\n/)
170
+ const starts = lines.map((l, i) => (l.trim() === '[[package]]' ? i : -1)).filter((i) => i >= 0)
171
+ starts.forEach((start, n) => {
172
+ const body = lines.slice(start + 1, starts[n + 1] ?? lines.length).join('\n')
173
+ const name = /^name\s*=\s*"([^"]+)"/m.exec(body)?.[1]
174
+ if (!name || packages.has(pyName(name))) return
175
+ const nameLine = start + 1 + body.split('\n').findIndex((l) => /^name\s*=/.test(l)) + 1
176
+ const source = /^type\s*=\s*"([^"]+)"/m.exec(tomlTable(`[x]\n${body}`, 'package.source'))?.[1] ?? 'registry'
177
+ const item = entry(name, /^version\s*=\s*"([^"]+)"/m.exec(body)?.[1] ?? null, file, nameLine, source)
178
+ const offset = start + 1
179
+ for (const dep of tomlEntries(`[x]\n${body}`, 'package.dependencies')) {
180
+ const inline = tomlInline(dep.value)
181
+ if (inline?.optional === true) continue
182
+ const spec = inline ? (typeof inline.version === 'string' ? inline.version : null) : /^"([^"]*)"/.exec(dep.value)?.[1] ?? null
183
+ item.requires.push({ name: pyName(dep.key), spec, line: offset + dep.line - 1 })
184
+ }
185
+ packages.set(pyName(name), item)
186
+ graph.set(pyName(name), item.requires.map((r) => r.name))
187
+ })
188
+ // The project itself is not in poetry.lock: its direct dependencies start the walk.
189
+ graph.set('\u0000root', direct.filter((k) => packages.has(k)))
190
+ walk(packages, graph, new Set(['\u0000root']))
191
+ return { file, packages }
192
+ }
193
+
194
+ /** Direct dependency names of these Python folders: [project].dependencies and [tool.poetry.dependencies]. */
195
+ function pythonDirectNames(view, dirs) {
196
+ const out = new Set()
197
+ for (const dir of dirs) {
198
+ const text = read(view, join(dir, 'pyproject.toml'))
199
+ if (!text) continue
200
+ for (const req of tomlArray(tomlTable(text, 'project'), 'dependencies')) { const m = /^\s*([A-Za-z0-9][A-Za-z0-9._-]*)/.exec(req); if (m) out.add(pyName(m[1])) }
201
+ for (const { key } of tomlEntries(text, 'tool.poetry.dependencies')) if (key !== 'python') out.add(pyName(key))
202
+ }
203
+ return [...out]
204
+ }
205
+
206
+ /** Exact pins (name==1.2.3) in a requirements file. They are direct. */
207
+ export function requirementPins(file, text) {
208
+ if (typeof text !== 'string') return null
209
+ const packages = new Map()
210
+ text.split(/\r?\n/).forEach((raw, i) => {
211
+ const m = /^\s*([A-Za-z0-9][A-Za-z0-9._-]*)(?:\[[^\]]*\])?\s*===?\s*([A-Za-z0-9.+!-]+)\s*(?:[;#\\].*)?$/.exec(raw)
212
+ if (!m || packages.has(pyName(m[1]))) return
213
+ packages.set(pyName(m[1]), { ...entry(m[1], m[2], file, i + 1, 'registry'), direct: true })
214
+ })
215
+ return packages.size ? { file, packages } : null
216
+ }
217
+
218
+ // --- .NET ------------------------------------------------------------------------------
219
+
220
+ /** First line of each "key": { in a JSON file. */
221
+ function jsonLines(text) {
222
+ const lines = new Map()
223
+ text.split(/\r?\n/).forEach((l, i) => { const m = /^\s*"([^"]+)"\s*:\s*\{/.exec(l); if (m && !lines.has(m[1])) lines.set(m[1], i + 1) })
224
+ return lines
225
+ }
226
+
227
+ /** packages.lock.json: every target framework, Direct, Transitive, Project and CentralTransitive. */
228
+ export function nugetLock(file, text) {
229
+ const data = json(text)
230
+ if (!data?.dependencies) return null
231
+ const lines = jsonLines(text), textLines = text.split(/\r?\n/)
232
+ const packages = new Map(), graph = new Map(), firstParty = new Set(['\u0000root'])
233
+ const roots = []
234
+ for (const deps of Object.values(data.dependencies)) {
235
+ for (const [name, info] of Object.entries(deps ?? {})) {
236
+ const key = name.toLowerCase()
237
+ if (info?.type === 'Project') { firstParty.add(key); roots.push(key) } else if (info?.type === 'Direct') roots.push(key)
238
+ if (!packages.has(key)) {
239
+ const item = entry(name, info?.resolved ?? null, file, lines.get(name), info?.type === 'Project' ? 'project' : 'registry')
240
+ packages.set(key, item)
241
+ graph.set(key, [])
242
+ }
243
+ const item = packages.get(key)
244
+ for (const [dep, spec] of Object.entries(info?.dependencies ?? {})) {
245
+ const dk = dep.toLowerCase()
246
+ if (!graph.get(key).includes(dk)) graph.get(key).push(dk)
247
+ if (!item.requires.some((r) => r.name.toLowerCase() === dk)) {
248
+ const needle = `"${dep}": ${JSON.stringify(spec)}`
249
+ item.requires.push({ name: dep, spec: String(spec), line: textLines.findIndex((l) => l.includes(needle)) + 1 || undefined })
250
+ }
251
+ }
252
+ }
253
+ }
254
+ graph.set('\u0000root', [...new Set(roots)])
255
+ walk(packages, graph, firstParty)
256
+ return { file, packages }
257
+ }
258
+
259
+ /** obj/project.assets.json: targets per framework, and the direct dependencies of the project. */
260
+ export function nugetAssets(file, text) {
261
+ const data = json(text)
262
+ if (!data?.targets) return null
263
+ const lines = jsonLines(text)
264
+ const packages = new Map(), graph = new Map(), firstParty = new Set(['\u0000root'])
265
+ for (const libs of Object.values(data.targets)) {
266
+ for (const [id, info] of Object.entries(libs ?? {})) {
267
+ const [name, version] = id.split('/')
268
+ const key = name.toLowerCase()
269
+ if (info?.type === 'project') firstParty.add(key)
270
+ if (!packages.has(key)) { packages.set(key, entry(name, version ?? null, file, lines.get(id), info?.type === 'project' ? 'project' : 'registry')); graph.set(key, []) }
271
+ for (const [dep, spec] of Object.entries(info?.dependencies ?? {})) {
272
+ const dk = dep.toLowerCase()
273
+ if (!graph.get(key).includes(dk)) graph.get(key).push(dk)
274
+ if (!packages.get(key).requires.some((r) => r.name.toLowerCase() === dk)) packages.get(key).requires.push({ name: dep, spec: String(spec) })
275
+ }
276
+ }
277
+ }
278
+ const direct = Object.values(data.project?.frameworks ?? {}).flatMap((f) => Object.keys(f?.dependencies ?? {})).map((n) => n.toLowerCase())
279
+ graph.set('\u0000root', [...new Set([...direct, ...[...firstParty].filter((k) => k !== '\u0000root')])])
280
+ walk(packages, graph, firstParty)
281
+ return { file, packages }
282
+ }
283
+
284
+ // --- Node.js ---------------------------------------------------------------------------
285
+
286
+ /** package-lock.json v2 and v3 (the packages map), with a v1 fallback (nested dependencies). roots: folders relative to the lock. */
287
+ export function npmLock(file, text, roots) {
288
+ const data = json(text)
289
+ if (!data) return null
290
+ const lines = jsonLines(text)
291
+ const packages = new Map()
292
+ const depsOf = (info) => Object.keys({ ...(info?.dependencies ?? {}), ...(info?.optionalDependencies ?? {}), ...(info?.peerDependencies ?? {}) })
293
+ const specsOf = (info) => Object.entries({ ...(info?.peerDependencies ?? {}), ...(info?.optionalDependencies ?? {}), ...(info?.dependencies ?? {}) })
294
+ .map(([name, spec]) => ({ name, spec: String(spec) }))
295
+ if (data.packages && typeof data.packages === 'object') {
296
+ const pk = data.packages
297
+ const MARK = 'node_modules/'
298
+ const isPkg = (key) => key.includes(MARK)
299
+ const nameOf = (key) => key.slice(key.lastIndexOf(MARK) + MARK.length)
300
+ const depth = (key) => key.split(MARK).length
301
+ const real = (key) => (pk[key]?.link && typeof pk[key].resolved === 'string' && pk[pk[key].resolved] ? pk[key].resolved : key)
302
+ const parent = (key) => { const at = key.lastIndexOf(`/${MARK}`); return at >= 0 ? key.slice(0, at) : '' }
303
+ const resolve = (from, name) => {
304
+ for (let base = from; ; base = parent(base)) {
305
+ const key = `${base ? `${base}/` : ''}${MARK}${name}`
306
+ if (pk[key]) return real(key)
307
+ if (!base) return null
308
+ }
309
+ }
310
+ // 1 entry per name: the copy nearest the root.
311
+ for (const [key, info] of Object.entries(pk)) {
312
+ if (!isPkg(key) || info?.link) continue
313
+ const name = nameOf(key), old = packages.get(name)
314
+ if (old && old._depth <= depth(key)) continue
315
+ const item = entry(name, info?.version ?? null, file, lines.get(key), typeof info?.resolved === 'string' && info.resolved.startsWith('file:') ? 'file' : 'registry')
316
+ item.requires = specsOf(info)
317
+ packages.set(name, Object.assign(item, { _depth: depth(key) }))
318
+ }
319
+ // Walk the folders. Workspace folders (no node_modules in the key) are first-party.
320
+ const queue = roots.filter((k) => pk[k]).map((k) => [k, null])
321
+ const seen = new Set()
322
+ for (let visits = 0; queue.length && visits < MAX_VISITS; visits++) {
323
+ const [key, via] = queue.shift()
324
+ if (seen.has(`${key}\u0000${via ?? ''}`)) continue
325
+ seen.add(`${key}\u0000${via ?? ''}`)
326
+ for (const dep of depsOf(pk[key])) {
327
+ const target = resolve(key, dep)
328
+ if (target === null) continue
329
+ if (!isPkg(target)) { queue.push([target, null]); continue }
330
+ const item = packages.get(nameOf(target))
331
+ if (!item) continue
332
+ if (via === null) item.direct = true
333
+ else if (!item.via.includes(via)) item.via.push(via)
334
+ queue.push([target, via ?? nameOf(target)])
335
+ }
336
+ }
337
+ for (const item of packages.values()) { delete item._depth; if (item.direct) item.via = [] }
338
+ return { file, packages }
339
+ }
340
+ // lockfileVersion 1: nested dependencies with requires. Only the versions and requirements are read.
341
+ const walkV1 = (deps, level) => {
342
+ for (const [name, info] of Object.entries(deps ?? {})) {
343
+ if (!packages.has(name)) {
344
+ const item = entry(name, info?.version ?? null, file, lines.get(name), 'registry')
345
+ item.requires = Object.entries(info?.requires ?? {}).map(([dep, spec]) => ({ name: dep, spec: String(spec) }))
346
+ packages.set(name, item)
347
+ }
348
+ if (level < 20) walkV1(info?.dependencies, level + 1)
349
+ }
350
+ }
351
+ walkV1(data.dependencies, 0)
352
+ return { file, packages }
353
+ }
package/lib/manifest.mjs CHANGED
@@ -6,19 +6,23 @@
6
6
  import { createHash } from 'node:crypto'
7
7
  import { existsSync, readFileSync } from 'node:fs'
8
8
  import { join } from 'node:path'
9
- import { CLI, PINS } from './contract.mjs'
9
+ import { AGENT_CHANNELS, AGENT_REGISTRY, CLI, PINS } from './contract.mjs'
10
10
  import { hunks, reverseAt, reverseByContent, splitLines } from './diff.mjs'
11
11
  import { checkPath } from './workspace.mjs'
12
+ import { HOOKS } from './hooks/pins.mjs'
12
13
 
13
14
  export const TOOL = '@uknows/setup'
14
15
  export const VERSION = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8')).version
15
16
  export const BOM = '\uFEFF'
16
17
  const NOTE = 'Written by npx @uknows/setup. To undo these changes, run npx @uknows/setup remove. This file has no secrets.'
17
18
  const STACKS = ['node', 'python', 'dotnet', 'java', 'go', 'php', 'ruby', 'otel']
19
+ /** services[].mode, written since 0.2.0. A 0.1.0 manifest has none. channel: the agent channel (--channel). */
20
+ const MODES = ['standard', 'otel', 'azure-monitor', 'channel']
18
21
  const UNDO = { npm: 'uninstall', pnpm: 'remove', yarn: 'remove' }
19
22
  /** The only flags an undo command may have: the ones setup itself writes. Everything else must be a package setup adds. */
20
- const UNDO_FLAGS = new Set(['-w', '--no-package-lock', '--package-lock-only', '--lockfile-only', '--ignore-scripts', '--mode=skip-build'])
21
- const PACKAGES = new Set([...PINS.node.packages, PINS.node.esmPackage])
23
+ const UNDO_FLAGS = new Set(['-w', '--no-package-lock', '--package-lock-only', '--lockfile-only', '--ignore-scripts', '--mode=skip-build', '--no-audit', '--no-fund'])
24
+ // The packages of the Node.js stack, and of the UKNOWS hooks for Azure Functions (stacks/functions-hooks.mjs).
25
+ const PACKAGES = new Set([...PINS.node.packages, PINS.node.esmPackage, ...HOOKS.node.packages.map(([name]) => name)])
22
26
  const SHA = /^[0-9a-f]{64}$/
23
27
 
24
28
  /** @param {Buffer|null} bytes */
@@ -41,7 +45,16 @@ export function readManifest(root) {
41
45
  function valid(m) {
42
46
  try {
43
47
  if (m?.schema !== 1 || ![m.services, m.changes, m.packages].every(Array.isArray)) return false
44
- for (const s of m.services) { checkPath(s.dir); if (!STACKS.includes(s.stack)) return false }
48
+ if (m.updates !== undefined && !RECORDED_BOTS.includes(m.updates)) return false
49
+ for (const s of m.services) {
50
+ checkPath(s.dir)
51
+ if (!STACKS.includes(s.stack) || (s.mode !== undefined && !MODES.includes(s.mode))) return false
52
+ // steps: the titles of the required steps in the repo that were left when setup ran (since 0.2.0). Optional.
53
+ if (s.steps !== undefined && !(Array.isArray(s.steps) && s.steps.every((t) => typeof t === 'string'))) return false
54
+ // channel: {name, registry} of a service in channel mode, and only there. A rerun reads them from here.
55
+ if ((s.mode === 'channel') !== (s.channel !== undefined)) return false
56
+ if (s.channel !== undefined && !(AGENT_CHANNELS.includes(s.channel?.name) && typeof s.channel?.registry === 'string' && AGENT_REGISTRY.test(s.channel.registry))) return false
57
+ }
45
58
  for (const c of m.changes) {
46
59
  checkPath(c.file)
47
60
  if (!c.hunks.every((h) => Number.isInteger(h.at) && [...h.before, ...h.after].every((l) => typeof l === 'string'))) return false
@@ -74,12 +87,19 @@ export function changeEntry({ file, before, after, bom }, beforeBytes, afterByte
74
87
  hunks: before === null ? [] : hunks(before, after) }
75
88
  }
76
89
 
77
- /** Add 1 run to a manifest (or start one). Services are merged by dir. */
78
- export function mergeManifest(old, { services, changes, packages }) {
90
+ /** The update bots that a manifest can record (updates: since 0.2.0, optional): what setup chose for the package route. */
91
+ export const RECORDED_BOTS = ['renovate', 'dependabot', 'none']
92
+
93
+ /**
94
+ * Add 1 run to a manifest (or start one). Services are merged by dir. updates: the update bot of this run, kept by a
95
+ * rerun (updates.mjs). Without one, the manifest keeps what it had.
96
+ */
97
+ export function mergeManifest(old, { services, changes, packages, updates }) {
79
98
  const kept = (old?.services ?? []).filter((s) => !services.some((n) => n.dir === s.dir))
99
+ const bot = RECORDED_BOTS.includes(updates) ? updates : RECORDED_BOTS.includes(old?.updates) ? old.updates : null
80
100
  return { schema: 1, tool: TOOL, version: VERSION, note: NOTE,
81
101
  services: [...kept, ...services].sort((a, b) => (a.dir < b.dir ? -1 : a.dir > b.dir ? 1 : 0)),
82
- changes: [...(old?.changes ?? []), ...changes], packages: [...(old?.packages ?? []), ...packages] }
102
+ changes: [...(old?.changes ?? []), ...changes], packages: [...(old?.packages ?? []), ...packages], ...(bot ? { updates: bot } : {}) }
83
103
  }
84
104
 
85
105
  /** Paths the dirty-tree check ignores: the manifest and every path it lists. */