@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.
Files changed (53) hide show
  1. package/LICENSE +190 -0
  2. package/README.md +209 -2
  3. package/bin/uknows-setup.mjs +14 -0
  4. package/lib/automate.mjs +1299 -0
  5. package/lib/check.mjs +327 -0
  6. package/lib/ci.mjs +21 -0
  7. package/lib/cli.mjs +251 -0
  8. package/lib/cloudid.mjs +456 -0
  9. package/lib/code.mjs +147 -0
  10. package/lib/context.mjs +126 -0
  11. package/lib/contract-docs.mjs +2736 -0
  12. package/lib/contract.mjs +149 -0
  13. package/lib/detect.mjs +627 -0
  14. package/lib/diff.mjs +147 -0
  15. package/lib/doc.mjs +98 -0
  16. package/lib/dockerfile.mjs +455 -0
  17. package/lib/endpoint.mjs +81 -0
  18. package/lib/engine.mjs +1753 -0
  19. package/lib/existing.mjs +562 -0
  20. package/lib/git.mjs +100 -0
  21. package/lib/hooks/UknowsOpenTelemetry.cs +118 -0
  22. package/lib/hooks/pins.mjs +28 -0
  23. package/lib/hooks/uknows-otel.cjs +82 -0
  24. package/lib/hooks/uknows_otel.py +179 -0
  25. package/lib/hosts.mjs +650 -0
  26. package/lib/locks.mjs +353 -0
  27. package/lib/manifest.mjs +132 -0
  28. package/lib/members.mjs +269 -0
  29. package/lib/plan.mjs +130 -0
  30. package/lib/settings.mjs +468 -0
  31. package/lib/signin.mjs +259 -0
  32. package/lib/stacks/azure.mjs +345 -0
  33. package/lib/stacks/channel.mjs +125 -0
  34. package/lib/stacks/dotnet.mjs +472 -0
  35. package/lib/stacks/functions-hooks.mjs +228 -0
  36. package/lib/stacks/functions.mjs +148 -0
  37. package/lib/stacks/guided.mjs +227 -0
  38. package/lib/stacks/index.mjs +18 -0
  39. package/lib/stacks/java.mjs +57 -0
  40. package/lib/stacks/k8s.mjs +31 -0
  41. package/lib/stacks/node.mjs +358 -0
  42. package/lib/stacks/otel.mjs +63 -0
  43. package/lib/stacks/python-bootstrap.mjs +71 -0
  44. package/lib/stacks/python-lock.mjs +105 -0
  45. package/lib/stacks/python-sets.mjs +143 -0
  46. package/lib/stacks/python.mjs +632 -0
  47. package/lib/toml.mjs +64 -0
  48. package/lib/types.mjs +208 -0
  49. package/lib/ui.mjs +225 -0
  50. package/lib/updates.mjs +398 -0
  51. package/lib/versions.mjs +115 -0
  52. package/lib/workspace.mjs +201 -0
  53. 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.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
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
+ }