@uknows/setup 0.0.0-stage → 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.
- package/LICENSE +190 -0
- package/README.md +209 -2
- package/bin/uknows-setup.mjs +14 -0
- package/lib/automate.mjs +1299 -0
- package/lib/check.mjs +327 -0
- package/lib/ci.mjs +21 -0
- package/lib/cli.mjs +251 -0
- package/lib/cloudid.mjs +456 -0
- package/lib/code.mjs +147 -0
- package/lib/context.mjs +126 -0
- package/lib/contract-docs.mjs +2736 -0
- package/lib/contract.mjs +149 -0
- package/lib/detect.mjs +627 -0
- package/lib/diff.mjs +147 -0
- package/lib/doc.mjs +98 -0
- package/lib/dockerfile.mjs +455 -0
- package/lib/endpoint.mjs +81 -0
- package/lib/engine.mjs +1753 -0
- package/lib/existing.mjs +562 -0
- package/lib/git.mjs +100 -0
- package/lib/hooks/UknowsOpenTelemetry.cs +118 -0
- package/lib/hooks/pins.mjs +28 -0
- package/lib/hooks/uknows-otel.cjs +82 -0
- package/lib/hooks/uknows_otel.py +179 -0
- package/lib/hosts.mjs +650 -0
- package/lib/locks.mjs +353 -0
- package/lib/manifest.mjs +132 -0
- package/lib/members.mjs +269 -0
- package/lib/plan.mjs +130 -0
- package/lib/settings.mjs +468 -0
- package/lib/signin.mjs +259 -0
- package/lib/stacks/azure.mjs +345 -0
- package/lib/stacks/channel.mjs +125 -0
- package/lib/stacks/dotnet.mjs +472 -0
- package/lib/stacks/functions-hooks.mjs +228 -0
- package/lib/stacks/functions.mjs +148 -0
- package/lib/stacks/guided.mjs +227 -0
- package/lib/stacks/index.mjs +18 -0
- package/lib/stacks/java.mjs +57 -0
- package/lib/stacks/k8s.mjs +31 -0
- package/lib/stacks/node.mjs +358 -0
- package/lib/stacks/otel.mjs +63 -0
- package/lib/stacks/python-bootstrap.mjs +71 -0
- package/lib/stacks/python-lock.mjs +105 -0
- package/lib/stacks/python-sets.mjs +143 -0
- package/lib/stacks/python.mjs +632 -0
- package/lib/toml.mjs +64 -0
- package/lib/types.mjs +208 -0
- package/lib/ui.mjs +225 -0
- package/lib/updates.mjs +398 -0
- package/lib/versions.mjs +115 -0
- package/lib/workspace.mjs +201 -0
- package/package.json +37 -4
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* A read view of the repo plus the edits staged in this run. Producers read
|
|
3
|
+
* through it, so a later producer sees the edits of an earlier one. Nothing
|
|
4
|
+
* here writes to disk: the engine writes ws.changes() on apply.
|
|
5
|
+
*
|
|
6
|
+
* Paths are repo-relative with '/' on every OS. '.' is the root.
|
|
7
|
+
*/
|
|
8
|
+
import { closeSync, existsSync, lstatSync, openSync, readFileSync, readSync, readdirSync } from 'node:fs'
|
|
9
|
+
import { homedir } from 'node:os'
|
|
10
|
+
import { dirname, join, posix, relative, resolve, sep } from 'node:path'
|
|
11
|
+
|
|
12
|
+
/** Folders that never hold a service or deploy file of the app. Dot folders are skipped too, except .github. */
|
|
13
|
+
export const SKIP_DIRS = new Set(['node_modules', 'vendor', 'venv', 'target', 'build', 'dist', 'out', 'bin', 'obj',
|
|
14
|
+
'__pycache__', 'coverage', 'test', 'tests', '__tests__', 'test_data', 'testdata', 'fixtures', 'examples', 'example', 'samples'])
|
|
15
|
+
const BOM = '\uFEFF'
|
|
16
|
+
const LIMIT = 20000
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* @typedef {Object} Change The net change to 1 file after every staged edit.
|
|
20
|
+
* @property {string} file
|
|
21
|
+
* @property {string|null} before Disk text before this run, BOM removed. null = new file.
|
|
22
|
+
* @property {string} after Text to write, BOM removed.
|
|
23
|
+
* @property {boolean} bom The disk file starts with a UTF-8 BOM. The engine writes it back.
|
|
24
|
+
* @property {string[]} why The why of each edit, in order.
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
export class Workspace {
|
|
28
|
+
/** @param {string} root absolute or relative path of the repo root */
|
|
29
|
+
constructor(root) {
|
|
30
|
+
this.root = resolve(root)
|
|
31
|
+
/** @type {Map<string, string>} */ this.overlay = new Map()
|
|
32
|
+
/** @type {Map<string, string|null>} */ this.original = new Map()
|
|
33
|
+
/** @type {Map<string, string[]>} */ this.whys = new Map()
|
|
34
|
+
/** True when files() stopped at its limit. */ this.truncated = false
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Absolute native path. */
|
|
38
|
+
abs(path) {
|
|
39
|
+
checkPath(path)
|
|
40
|
+
return path === '.' ? this.root : join(this.root, ...path.split('/'))
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
stat(path) {
|
|
44
|
+
try { return lstatSync(this.abs(path)) } catch { return null }
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
exists(path) { return this.overlay.has(path) || this.stat(path) !== null }
|
|
48
|
+
|
|
49
|
+
/** A real folder (not a link). */
|
|
50
|
+
isDir(path) { return Boolean(this.stat(path)?.isDirectory()) }
|
|
51
|
+
|
|
52
|
+
/** Current text: staged edits first, else disk. BOM removed. null when missing, a link or not a file. */
|
|
53
|
+
read(path) {
|
|
54
|
+
checkPath(path)
|
|
55
|
+
return this.overlay.has(path) ? this.overlay.get(path) : this.disk(path)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Text on disk, ignoring staged edits. BOM removed. */
|
|
59
|
+
disk(path) {
|
|
60
|
+
const stat = this.stat(path)
|
|
61
|
+
if (!stat?.isFile()) return null
|
|
62
|
+
const text = readFileSync(this.abs(path), 'utf8')
|
|
63
|
+
return text.startsWith(BOM) ? text.slice(1) : text
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
hasBom(path) {
|
|
67
|
+
if (!this.stat(path)?.isFile()) return false
|
|
68
|
+
const fd = openSync(this.abs(path), 'r')
|
|
69
|
+
try {
|
|
70
|
+
const head = Buffer.alloc(3)
|
|
71
|
+
return readSync(fd, head, 0, 3, 0) === 3 && head[0] === 0xef && head[1] === 0xbb && head[2] === 0xbf
|
|
72
|
+
} finally { closeSync(fd) }
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** Names in a folder (files and folders, links included), plus staged new files. Sorted. */
|
|
76
|
+
list(dir = '.') {
|
|
77
|
+
const names = new Set()
|
|
78
|
+
try { for (const name of readdirSync(this.abs(dir))) names.add(name) } catch { /* missing */ }
|
|
79
|
+
const prefix = dir === '.' ? '' : `${dir}/`
|
|
80
|
+
for (const path of this.overlay.keys()) {
|
|
81
|
+
if (path.startsWith(prefix) && !path.slice(prefix.length).includes('/')) names.add(path.slice(prefix.length))
|
|
82
|
+
}
|
|
83
|
+
return [...names].sort(byCodePoint)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Every regular file under dir, recursive, sorted. Skips links, SKIP_DIRS
|
|
88
|
+
* and dot folders except .github. Includes staged new files.
|
|
89
|
+
*/
|
|
90
|
+
files(dir = '.') {
|
|
91
|
+
const out = new Set()
|
|
92
|
+
const walk = (rel) => {
|
|
93
|
+
let entries
|
|
94
|
+
try { entries = readdirSync(this.abs(rel), { withFileTypes: true }) } catch { return }
|
|
95
|
+
for (const entry of entries) {
|
|
96
|
+
if (out.size >= LIMIT) { this.truncated = true; return }
|
|
97
|
+
const path = rel === '.' ? entry.name : `${rel}/${entry.name}`
|
|
98
|
+
if (entry.isSymbolicLink()) continue
|
|
99
|
+
if (entry.isDirectory()) {
|
|
100
|
+
if (SKIP_DIRS.has(entry.name) || (entry.name.startsWith('.') && entry.name !== '.github')) continue
|
|
101
|
+
walk(path)
|
|
102
|
+
} else if (entry.isFile()) out.add(path)
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
walk(dir)
|
|
106
|
+
const prefix = dir === '.' ? '' : `${dir}/`
|
|
107
|
+
for (const path of this.overlay.keys()) if (path.startsWith(prefix)) out.add(path)
|
|
108
|
+
return [...out].sort(byCodePoint)
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Stage an edit. Throws when edit.before is not the current text, which
|
|
113
|
+
* means 2 producers raced on the same file.
|
|
114
|
+
* @param {import('./types.mjs').Edit} edit
|
|
115
|
+
*/
|
|
116
|
+
stage(edit) {
|
|
117
|
+
const current = this.read(edit.file)
|
|
118
|
+
const stale = edit.before === null ? current !== null || this.exists(edit.file) : current !== edit.before
|
|
119
|
+
if (stale) throw new Error(`Edit for ${edit.file} is out of date`)
|
|
120
|
+
if (edit.after === current) return
|
|
121
|
+
if (!this.original.has(edit.file)) this.original.set(edit.file, current)
|
|
122
|
+
this.overlay.set(edit.file, edit.after)
|
|
123
|
+
this.whys.set(edit.file, [...(this.whys.get(edit.file) ?? []), edit.why])
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** Net changes, in the order files were first staged. Files that ended up unchanged are left out. */
|
|
127
|
+
changes() {
|
|
128
|
+
const out = []
|
|
129
|
+
for (const [file, before] of this.original) {
|
|
130
|
+
const after = this.overlay.get(file)
|
|
131
|
+
if (after === before) continue
|
|
132
|
+
out.push({ file, before, after, bom: before !== null && this.hasBom(file), why: this.whys.get(file) ?? [] })
|
|
133
|
+
}
|
|
134
|
+
return out
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/** @param {string} root */
|
|
139
|
+
export const createWorkspace = (root) => new Workspace(root)
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* @typedef {Object} RepoView A read view of the whole git repo around the root, for --path runs in a monorepo.
|
|
143
|
+
* @property {() => string[]} files
|
|
144
|
+
* @property {(path: string) => string|null} read
|
|
145
|
+
* @property {(path: string) => boolean} exists
|
|
146
|
+
* @property {(path: string) => import('node:fs').Stats|null} stat
|
|
147
|
+
* @property {(dir?: string) => string[]} list Names in 1 folder, as Workspace.list. A folder above the root reads as '../x'.
|
|
148
|
+
* @property {string} top The repo top, relative to the root: '.', '..' or '../..'.
|
|
149
|
+
*/
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* The files of the whole git repo when the root is a folder inside it. Paths
|
|
153
|
+
* stay relative to the root: a file above it reads as '../../k8s/api.yaml'.
|
|
154
|
+
* Inside the root, reads go through ws, so staged edits count. Only folders
|
|
155
|
+
* up to 5 levels above the root count, and never the home folder, which can
|
|
156
|
+
* be a dotfiles repo. Nothing here writes.
|
|
157
|
+
* @param {Workspace} ws
|
|
158
|
+
* @returns {RepoView}
|
|
159
|
+
*/
|
|
160
|
+
export function repoView(ws) {
|
|
161
|
+
let top = null
|
|
162
|
+
for (let dir = ws.root, n = 0; n <= 5 && dir !== homedir(); dir = dirname(dir), n++) {
|
|
163
|
+
if (existsSync(join(dir, '.git'))) { top = dir; break }
|
|
164
|
+
if (dirname(dir) === dir) break
|
|
165
|
+
}
|
|
166
|
+
const view = { files: () => ws.files(), read: (path) => ws.read(path), exists: (path) => ws.exists(path), stat: (path) => ws.stat(path),
|
|
167
|
+
list: (dir = '.') => ws.list(dir), top: '.' }
|
|
168
|
+
if (!top || top === ws.root) return view
|
|
169
|
+
const outer = createWorkspace(top)
|
|
170
|
+
const prefix = relative(top, ws.root).split(sep).join('/')
|
|
171
|
+
const above = (path) => path.startsWith('../')
|
|
172
|
+
const toTop = (path) => posix.normalize(`${prefix}/${path}`)
|
|
173
|
+
let files = null
|
|
174
|
+
return {
|
|
175
|
+
files: () => (files ??= [...new Set([...ws.files(), ...outer.files().map((p) => posix.relative(prefix, p))])].sort(byCodePoint)),
|
|
176
|
+
read: (path) => (above(path) ? outer.read(toTop(path)) : ws.read(path)),
|
|
177
|
+
exists: (path) => (above(path) ? outer.exists(toTop(path)) : ws.exists(path)),
|
|
178
|
+
stat: (path) => (above(path) ? outer.stat(toTop(path)) : ws.stat(path)),
|
|
179
|
+
list: (dir = '.') => (dir === '..' || above(dir) ? outer.list(toTop(dir)) : ws.list(dir)),
|
|
180
|
+
top: prefix.split('/').map(() => '..').join('/'),
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/** Folders from dir up to top, nearest first: upFrom('a/b', '..') is ['a/b', 'a', '.', '..']. */
|
|
185
|
+
export function upFrom(dir, top = '.') {
|
|
186
|
+
const out = []
|
|
187
|
+
for (let d = dir; out.length < 64; d = d === '.' ? '..' : /^\.\.(\/\.\.)*$/.test(d) ? `${d}/..` : d.includes('/') ? d.slice(0, d.lastIndexOf('/')) : '.') {
|
|
188
|
+
out.push(d)
|
|
189
|
+
if (d === top) break
|
|
190
|
+
}
|
|
191
|
+
return out
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
export function checkPath(path) {
|
|
195
|
+
if (typeof path !== 'string' || !path || path.includes('\\') || path.startsWith('/') || /^[A-Za-z]:/.test(path)
|
|
196
|
+
|| (path !== '.' && path.split('/').some((part) => !part || part === '.' || part === '..'))) {
|
|
197
|
+
throw new Error(`Not a repo-relative path: ${path}`)
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function byCodePoint(a, b) { return a < b ? -1 : a > b ? 1 : 0 }
|
package/package.json
CHANGED
|
@@ -1,6 +1,39 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@uknows/setup",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "Add standard OpenTelemetry to your app in 1 command, so it sends traces and logs to UKNOWS. Shows the change, asks first, can undo.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"uknows",
|
|
7
|
+
"opentelemetry",
|
|
8
|
+
"otel",
|
|
9
|
+
"apm",
|
|
10
|
+
"tracing",
|
|
11
|
+
"observability",
|
|
12
|
+
"setup",
|
|
13
|
+
"cli"
|
|
14
|
+
],
|
|
15
|
+
"homepage": "https://uknows.my/docs",
|
|
16
|
+
"license": "Apache-2.0",
|
|
17
|
+
"author": "UKNOWS",
|
|
18
|
+
"type": "module",
|
|
19
|
+
"bin": {
|
|
20
|
+
"uknows-setup": "bin/uknows-setup.mjs"
|
|
21
|
+
},
|
|
22
|
+
"files": [
|
|
23
|
+
"bin/",
|
|
24
|
+
"lib/",
|
|
25
|
+
"README.md",
|
|
26
|
+
"LICENSE"
|
|
27
|
+
],
|
|
28
|
+
"engines": {
|
|
29
|
+
"node": ">=18"
|
|
30
|
+
},
|
|
31
|
+
"publishConfig": {
|
|
32
|
+
"access": "public",
|
|
33
|
+
"registry": "https://registry.npmjs.org/"
|
|
34
|
+
},
|
|
35
|
+
"scripts": {
|
|
36
|
+
"test": "node --test test/*.test.mjs",
|
|
37
|
+
"prepublishOnly": "node tools/prepublish.mjs"
|
|
38
|
+
}
|
|
39
|
+
}
|