From ba743f3d19b31d5956967becf9f95a076073a73c Mon Sep 17 00:00:00 2001
From: Max Howell
Date: Thu, 18 May 2023 10:44:57 -0400
Subject: [PATCH 01/23] use libtea
---
.github/workflows/ci.yml | 2 +-
deno.jsonc | 16 +-
import-map.json | 21 --
scripts/repair.ts | 11 +-
src/app.dump.ts | 19 +-
src/app.exec.ts | 22 +-
src/app.help.ts | 7 +-
src/app.magic.ts | 8 +-
src/app.main.ts | 41 ++-
src/app.provides.ts | 4 +-
src/app.ts | 33 +-
src/args.ts | 24 +-
src/hooks/index.ts | 75 ++---
src/hooks/useCache.ts | 31 --
src/hooks/useCellar.ts | 96 ------
src/hooks/useConfig.ts | 199 +++++++-----
src/hooks/useDownload.ts | 176 ----------
src/hooks/useErrorHandler.ts | 70 ++--
src/hooks/useExec.ts | 70 ++--
src/hooks/useFetch.ts | 8 -
src/hooks/useInventory.ts | 59 ----
src/hooks/useLogger.ts | 59 ++--
src/hooks/useMoustaches.ts | 45 ---
src/hooks/useOffLicense.ts | 28 --
src/hooks/usePackageYAML.ts | 18 +-
src/hooks/usePantry.ts | 331 -------------------
src/hooks/usePrefix.ts | 16 -
src/hooks/useRun.ts | 8 +-
src/hooks/useShellEnv.ts | 159 ---------
src/hooks/useSync.ts | 77 -----
src/hooks/useVersion.ts | 3 +-
src/hooks/useVirtualEnv.ts | 46 +--
src/init.ts | 103 ------
src/prefab/README.md | 7 -
src/prefab/hydrate.ts | 154 ---------
src/prefab/index.ts | 11 -
src/prefab/install.ts | 163 ----------
src/prefab/link.ts | 52 ---
src/prefab/resolve.ts | 50 ---
src/types.ts | 65 ----
src/utils/error.ts | 174 ----------
src/utils/hacks.ts | 47 ---
src/utils/index.ts | 202 ------------
src/utils/pkg.ts | 55 ----
src/utils/safe-utils.ts | 9 -
src/utils/semver.ts | 325 -------------------
src/vendor/Path.ts | 447 --------------------------
src/vendor/PathUtils.ts | 40 ---
src/vendor/README.md | 20 --
tests/functional/args.test.ts | 64 ++--
tests/functional/devenv.test.ts | 29 +-
tests/functional/exec.test.ts | 6 +-
tests/functional/magic.test.ts | 8 +-
tests/functional/provides.test.ts | 4 +-
tests/functional/repl.test.ts | 6 +-
tests/functional/script.test.ts | 5 +-
tests/functional/suggestions.test.ts | 5 +-
tests/functional/sync.test.ts | 11 +-
tests/functional/testUtils.ts | 54 ++--
tests/integration.suite.ts | 6 +-
tests/integration/magic.test.ts | 4 +-
tests/integration/package.yml.test.ts | 6 +-
tests/integration/tea.ln.test.ts | 12 +-
tests/integration/tea.scripts.test.ts | 2 +-
tests/unit/cache.test.ts | 42 ---
tests/unit/error.test.ts | 51 ---
tests/unit/fetch.test.ts | 27 --
tests/unit/hydrate.test.ts | 82 -----
tests/unit/path-utils.test.ts | 48 ---
tests/unit/path.test.ts | 84 -----
tests/unit/pkgutils.test.ts | 94 ------
tests/unit/semver.test.ts | 216 -------------
tests/unit/useErrorHandler.test.ts | 22 +-
tests/unit/utils.test.ts | 56 ----
74 files changed, 512 insertions(+), 4138 deletions(-)
delete mode 100644 import-map.json
delete mode 100644 src/hooks/useCache.ts
delete mode 100644 src/hooks/useCellar.ts
delete mode 100644 src/hooks/useDownload.ts
delete mode 100644 src/hooks/useFetch.ts
delete mode 100644 src/hooks/useInventory.ts
delete mode 100644 src/hooks/useMoustaches.ts
delete mode 100644 src/hooks/useOffLicense.ts
delete mode 100644 src/hooks/usePantry.ts
delete mode 100644 src/hooks/usePrefix.ts
delete mode 100644 src/hooks/useShellEnv.ts
delete mode 100644 src/hooks/useSync.ts
delete mode 100644 src/init.ts
delete mode 100644 src/prefab/README.md
delete mode 100644 src/prefab/hydrate.ts
delete mode 100644 src/prefab/index.ts
delete mode 100644 src/prefab/install.ts
delete mode 100644 src/prefab/link.ts
delete mode 100644 src/prefab/resolve.ts
delete mode 100644 src/types.ts
delete mode 100644 src/utils/error.ts
delete mode 100644 src/utils/hacks.ts
delete mode 100644 src/utils/index.ts
delete mode 100644 src/utils/pkg.ts
delete mode 100644 src/utils/safe-utils.ts
delete mode 100644 src/utils/semver.ts
delete mode 100644 src/vendor/Path.ts
delete mode 100644 src/vendor/PathUtils.ts
delete mode 100644 src/vendor/README.md
delete mode 100644 tests/unit/cache.test.ts
delete mode 100644 tests/unit/error.test.ts
delete mode 100644 tests/unit/fetch.test.ts
delete mode 100644 tests/unit/hydrate.test.ts
delete mode 100644 tests/unit/path-utils.test.ts
delete mode 100644 tests/unit/path.test.ts
delete mode 100644 tests/unit/pkgutils.test.ts
delete mode 100644 tests/unit/semver.test.ts
delete mode 100644 tests/unit/utils.test.ts
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index fb70be9b2..f73cfc4e0 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -48,7 +48,7 @@ jobs:
steps:
- uses: actions/checkout@v3
- uses: teaxyz/setup@v0
- - run: deno lint src/*/**.ts
+ - run: deno lint
typecheck:
runs-on: ubuntu-latest
diff --git a/deno.jsonc b/deno.jsonc
index 03bcd7288..ea0506b8a 100644
--- a/deno.jsonc
+++ b/deno.jsonc
@@ -23,10 +23,22 @@
]
}
},
+ "lint": {
+ "include": ["src/", "scripts/"]
+ },
"tea": {
"dependencies": {
- "deno.land": "^1.31.1"
+ "deno.land": "^1.33.3"
}
},
- "importMap": "import-map.json"
+ "imports": {
+ "tea": "https://raw.github.com/teaxyz/lib/v0/mod.ts",
+ "tea/": "https://raw.github.com/teaxyz/lib/v0/src/",
+ "hooks": "./src/hooks/index.ts",
+ "deno/": "https://deno.land/std@0.182.0/",
+ "is-what": "https://deno.land/x/is_what@v4.1.8/src/index.ts",
+ "cliffy/": "https://deno.land/x/cliffy@v0.25.7/",
+ "outdent": "https://deno.land/x/outdent@v0.8.0/mod.ts",
+ "jsonc": "https://deno.land/x/jsonc_parser@v0.0.1/mod.ts"
+ }
}
diff --git a/import-map.json b/import-map.json
deleted file mode 100644
index 437ac7994..000000000
--- a/import-map.json
+++ /dev/null
@@ -1,21 +0,0 @@
-{
- "imports": {
- "path": "./src/vendor/Path.ts",
- "path-utils": "./src/vendor/PathUtils.ts",
- "types": "./src/types.ts",
- "hooks": "./src/hooks/index.ts",
- "hooks/": "./src/hooks/",
- "prefab": "./src/prefab/index.ts",
- "prefab/": "./src/prefab/",
- "deno/": "https://deno.land/std@0.182.0/",
- "semver": "./src/utils/semver.ts",
- "utils": "./src/utils/index.ts",
- "utils/": "./src/utils/",
- "is_what": "https://deno.land/x/is_what@v4.1.8/src/index.ts",
- "cliffy/": "https://deno.land/x/cliffy@v0.25.7/",
- "s3": "https://deno.land/x/s3@0.5.0/mod.ts",
- "outdent": "https://deno.land/x/outdent@v0.8.0/mod.ts",
- "rimbu/": "https://deno.land/x/rimbu@0.13.2/",
- "jsonc": "https://deno.land/x/jsonc_parser@v0.0.1/mod.ts"
- }
-}
diff --git a/scripts/repair.ts b/scripts/repair.ts
index 15e62595f..a454f75b7 100755
--- a/scripts/repair.ts
+++ b/scripts/repair.ts
@@ -1,13 +1,12 @@
#!/usr/bin/env -S deno run -A
-import { init } from "../src/init.ts";
-import { useCellar } from "hooks"
-import { Installation } from "types"
-import { link } from "prefab"
-import * as semver from "semver"
+import { hooks, semver, Installation, prefab } from "tea"
+import { useConfig } from "hooks"
+const { useCellar } = hooks
+const { link } = prefab
if (import.meta.main) {
- init()
+ useConfig()
for (const project of Deno.args) {
await repairLinks(project)
diff --git a/src/app.dump.ts b/src/app.dump.ts
index 2ebe241ce..fd7fcd17d 100644
--- a/src/app.dump.ts
+++ b/src/app.dump.ts
@@ -1,6 +1,7 @@
-import { useEnv, usePrint } from "hooks"
-import { flatmap } from "utils"
-import { isPlainObject } from "is_what"
+import { useConfig, usePrint } from "hooks"
+import { isPlainObject } from "is-what"
+import { utils } from "tea"
+const { flatmap } = utils
//TODO should read from the shell configuration files to get originals properly
//TODO don’t wait on each print, instead chain the promises to be more time-efficient
@@ -11,7 +12,7 @@ interface Parameters {
}
export default async function dump({ env, shell }: Parameters) {
- const { TEA_REWIND, getEnvAsObject } = useEnv();
+ const { TEA_REWIND, obj: oldenv } = useConfig().env
const { print } = usePrint()
const [set, unset]= (() => {
@@ -37,11 +38,9 @@ export default async function dump({ env, shell }: Parameters) {
const is_env = env['SRCROOT']
if (is_env) {
- const oldenv = getEnvAsObject()
-
// first rewind the env to the original state
- if (oldenv['TEA_REWIND']) {
- const rewind = JSON.parse(oldenv['TEA_REWIND']) as { revert: Record, unset: string[] }
+ if (TEA_REWIND) {
+ const rewind = JSON.parse(TEA_REWIND) as { revert: Record, unset: string[] }
delete oldenv['TEA_REWIND']
for (const key of rewind.unset) {
@@ -59,7 +58,7 @@ export default async function dump({ env, shell }: Parameters) {
}
// now calculate the new rewind
- const TEA_REWIND = (() => {
+ const new_TEA_REWIND = (() => {
const revert: Record = {}
const unset: string[] = []
for (const key of Object.keys(env)) {
@@ -79,7 +78,7 @@ export default async function dump({ env, shell }: Parameters) {
for (const [key, value] of Object.entries(env)) {
if (value) await print(set(key, value))
}
- await print(set('TEA_REWIND', TEA_REWIND))
+ await print(set('TEA_REWIND', new_TEA_REWIND))
} else {
const unwind = flatmap(TEA_REWIND, JSON.parse) as { revert: Record, unset: string[] }
diff --git a/src/app.exec.ts b/src/app.exec.ts
index c814883c1..c16f8b3b7 100644
--- a/src/app.exec.ts
+++ b/src/app.exec.ts
@@ -1,24 +1,21 @@
-import { pkg as pkgutils, TeaError, chuzzle } from "utils"
-import { ExitError, Installation } from "types"
-import { useEnv, useConfig, useRun } from "hooks"
-import { RunError } from "hooks/useRun.ts"
-import { gray, red, teal } from "hooks/useLogger.ts"
+import { useConfig, useRun, useLogger, RunError, Verbosity, ExitError } from "hooks"
+import { Installation, Path, utils, TeaError } from "tea"
import { basename } from "deno/path/mod.ts"
-import { isNumber } from "is_what"
-import Path from "path"
+import { isNumber } from "is-what"
export default async function(cmd: string[], env: Record) {
- const { TEA_FORK_BOMB_PROTECTOR } = useEnv()
+ const { TEA_FORK_BOMB_PROTECTOR } = useConfig().env
+ const { red, teal } = useLogger()
// ensure we cannot fork bomb the user since this is basically the worst thing tea/cli can do
- let nobomb = chuzzle(parseInt(TEA_FORK_BOMB_PROTECTOR ?? '0')) ?? 0
+ let nobomb = parseInt(TEA_FORK_BOMB_PROTECTOR ?? '0').chuzzle() ?? 0
env['TEA_FORK_BOMB_PROTECTOR'] = `${++nobomb}`
if (nobomb > 20) throw new Error("FORK BOMB KILL SWITCH ACTIVATED")
try {
await useRun({cmd, env})
} catch (err) {
- const { debug } = useConfig()
+ const debug = useConfig().modifiers.verbosity >= Verbosity.debug
const arg0 = cmd?.[0]
if (err instanceof TeaError) {
@@ -44,8 +41,9 @@ export default async function(cmd: string[], env: Record) {
}
export async function repl(installations: Installation[], env: Record) {
- const { SHELL } = useEnv()
- const pkgs_str = () => installations.map(({pkg}) => gray(pkgutils.str(pkg))).join(", ")
+ const { SHELL } = useConfig().env
+ const { gray } = useLogger()
+ const pkgs_str = () => installations.map(({pkg}) => gray(utils.pkg.str(pkg))).join(", ")
// going to stderr so that we don’t potentially break (nonsensical) pipe scenarios, eg.
// tea -E | env
diff --git a/src/app.help.ts b/src/app.help.ts
index 3389471fe..40a28748e 100644
--- a/src/app.help.ts
+++ b/src/app.help.ts
@@ -1,11 +1,12 @@
+import { Verbosity } from "./hooks/useConfig.ts"
import { useConfig, usePrint } from "hooks"
-import { undent } from "utils"
+import undent from "outdent"
export default async function help() {
- const { verbose } = useConfig()
+ const { modifiers: { verbosity } } = useConfig()
const { print } = usePrint()
- if (!verbose) {
+ if (verbosity < Verbosity.loud) {
// 10| 20| 30| 40| 50| 60| 70| | 80|
await print(undent`
usage:
diff --git a/src/app.magic.ts b/src/app.magic.ts
index 292dfa12b..69894db35 100644
--- a/src/app.magic.ts
+++ b/src/app.magic.ts
@@ -1,10 +1,10 @@
import { basename } from "deno/path/mod.ts"
-import { undent } from "utils"
-import Path from "path"
-import { useEnv } from "./hooks/useConfig.ts"
+import { useConfig } from "hooks"
+import undent from "outdent"
+import { Path } from "tea"
export default function(self: Path, shell?: string) {
- const { SHELL } = useEnv()
+ const { SHELL } = useConfig().env
shell ??= basename(SHELL ?? "unknown")
const d = self.parent()
diff --git a/src/app.main.ts b/src/app.main.ts
index da77fca6c..d9119c851 100644
--- a/src/app.main.ts
+++ b/src/app.main.ts
@@ -1,25 +1,24 @@
-import { usePrefix, useExec, useVirtualEnv, useVersion, useSync, usePrint, useConfig, useEnv } from "hooks"
-import dump from "./app.dump.ts"
-import help from "./app.help.ts"
-import provides from "./app.provides.ts";
-import magic from "./app.magic.ts"
-import exec, { repl } from "./app.exec.ts"
-import { pkg as pkgutils, flatmap } from "utils"
-import Path from "path"
-import { Verbosity } from "./types.ts"
-import * as semver from "semver"
+import { usePrefix, useExec, useVirtualEnv, useVersion, usePrint, useConfig } from "hooks"
import { VirtualEnv } from "./hooks/useVirtualEnv.ts"
+import { Verbosity } from "./hooks/useConfig.ts"
import { basename } from "deno/path/mod.ts"
-import { Args } from "./args.ts";
+import exec, { repl } from "./app.exec.ts"
+import { Path, utils, semver, hooks } from "tea"
+import provides from "./app.provides.ts"
+import magic from "./app.magic.ts"
+import dump from "./app.dump.ts"
+import help from "./app.help.ts"
+import { Args } from "./args.ts"
+const { flatmap } = utils
+const { useSync } = hooks
export async function run(args: Args) {
- const { print } = usePrint();
- const { execPath } = useConfig()
- const { PATH, SHELL } = useEnv()
+ const { print } = usePrint()
+ const { arg0: execPath, env: { PATH, SHELL } } = useConfig()
if (args.cd) {
const chdir = args.cd
- console.verbose({ chdir })
+ console.log({ chdir })
Deno.chdir(chdir.string)
}
@@ -74,7 +73,7 @@ export async function run(args: Args) {
break
case "dump": {
env['PATH'] = full_path().join(':')
- env["TEA_PKGS"] = pkgs.map(pkgutils.str).join(":").trim()
+ env["TEA_PKGS"] = pkgs.map(utils.pkg.str).join(":").trim()
env["TEA_PREFIX"] ??= usePrefix().string
env["TEA_VERSION"] = useVersion()
@@ -103,7 +102,7 @@ function announce(self: Path) {
const prefix = usePrefix().string
const version = useVersion()
- switch (useConfig().verbosity) {
+ switch (useConfig().modifiers.verbosity) {
case Verbosity.debug:
if (self.basename() == "deno") {
console.debug({ deno: self.string, prefix, import: import.meta, tea: version })
@@ -117,7 +116,7 @@ function announce(self: Path) {
}
function injection({ args, inject }: Args) {
- const { TEA_FILES, TEA_PKGS, SRCROOT, VERSION } = useEnv()
+ const { TEA_FILES, TEA_PKGS, SRCROOT, VERSION } = useConfig().env
const teaPkgs = TEA_PKGS?.trim()
//TODO if TEA_PKGS then extract virtual-env from that, don’t reinterpret it
@@ -133,7 +132,7 @@ function injection({ args, inject }: Args) {
cwd = file.parent()
}
- if (useConfig().keepGoing) {
+ if (useConfig().modifiers.keepGoing) {
return useVirtualEnv(cwd).swallow(/^not-found/)
} else if (teaPkgs) {
/// if an env is defined then we still are going to try to read it
@@ -158,7 +157,7 @@ function injection({ args, inject }: Args) {
//TODO anything that isn’t an absolute path will crash
return {
env: {},
- pkgs: TEA_PKGS!.split(":").map(pkgutils.parse),
+ pkgs: TEA_PKGS!.split(":").map(utils.pkg.parse),
teafiles: TEA_FILES.split(":").map(x => new Path(x)),
srcroot: new Path(SRCROOT),
version: flatmap(VERSION, semver.parse)
@@ -180,7 +179,7 @@ export function wut(args: Args): 'dump' | 'exec' | 'repl' | 'env' | 'dryrun' {
return true
})()
- if (useConfig().dryrun) {
+ if (useConfig().modifiers.dryrun) {
return 'dryrun'
} else if (stack_mode) {
return 'dump'
diff --git a/src/app.provides.ts b/src/app.provides.ts
index 44cf40e97..f52674552 100644
--- a/src/app.provides.ts
+++ b/src/app.provides.ts
@@ -1,5 +1,5 @@
-import { which } from "hooks/useExec.ts"
-import { ExitError } from "./types.ts"
+import { ExitError } from "./hooks/useErrorHandler.ts"
+import { which } from "./hooks/useExec.ts"
export default async function provides(args: string[]) {
let status = 0;
diff --git a/src/app.ts b/src/app.ts
index d2592bb5b..c29db0086 100644
--- a/src/app.ts
+++ b/src/app.ts
@@ -1,21 +1,40 @@
+import useConfig, { Config, ConfigDefault, Verbosity } from "./hooks/useConfig.ts"
+import { parseArgs, UsageError } from "./args.ts"
import { useErrorHandler } from "hooks"
-import help from "./app.help.ts"
-import { UsageError } from "utils"
import { run } from "./app.main.ts"
-import { parseArgs } from "./args.ts";
-import { init } from "./init.ts";
-import useConfig from "./hooks/useConfig.ts";
+import help from "./app.help.ts"
try {
const [args, flags] = parseArgs(Deno.args, Deno.execPath())
- init(flags)
+
+ const config = ConfigDefault(flags)
+
+ config.logger.prefix = (!Deno.isatty(Deno.stdout.rid) || Deno.env.get("CI")) ? "tea:" : undefined
+ useConfig(config)
+ applyVerbosity(config)
+
await run(args)
+
} catch (err) {
if (err instanceof UsageError) {
- if (!useConfig().silent) await help()
+ console.error(err.message)
+ await help()
Deno.exit(64)
} else {
const code = await useErrorHandler(err)
Deno.exit(code)
}
}
+
+
+function applyVerbosity(config: Config) {
+ function noop() {}
+ if (config!.modifiers.verbosity < Verbosity.debug) console.debug = noop
+ if (config!.modifiers.verbosity < Verbosity.loud) console.log = noop
+ if (config!.modifiers.verbosity < Verbosity.normal) {
+ console.info = noop
+ console.warn = noop
+ console.log = noop
+ console.error = noop
+ }
+}
diff --git a/src/args.ts b/src/args.ts
index e6155ae3b..38a0f957f 100644
--- a/src/args.ts
+++ b/src/args.ts
@@ -1,7 +1,5 @@
-import Path from "path"
-import { chuzzle, pkg, validate_str } from "utils"
-import { PackageSpecification } from "types"
-import { TeaError } from "utils"
+import { utils, PackageSpecification, Path } from "tea"
+const { pkg, validate } = utils
export type Args = {
cd?: Path
@@ -55,10 +53,10 @@ export function parseArgs(args: string[], arg0: string): [Args, Flags, Error?] {
const it = args[Symbol.iterator]()
for (const arg of it) {
- const barf = (arg_?: string) => { throw new TeaError('not-found: arg', {arg: arg_ ?? arg}) }
+ const barf = (arg_?: string) => { throw new UsageError(arg_ ?? arg) }
if (arg == '+' || arg == '-') {
- throw new TeaError('not-found: arg', {arg})
+ throw new UsageError(arg)
}
if (arg.startsWith('+')) {
@@ -79,13 +77,13 @@ export function parseArgs(args: string[], arg0: string): [Args, Flags, Error?] {
flags.verbosity = 1
break
}
- const bi = chuzzle(parseInt(value) + 1)
+ const bi = (parseInt(value) + 1).chuzzle()
if (bi !== undefined) {
flags.verbosity = bi
break
}
const bv = parseBool(value)
- if (bv === undefined) throw new TeaError('not-found: arg', {arg})
+ if (bv === undefined) throw new UsageError(arg)
flags.verbosity = bv ? 1 : 0
} break
case 'debug':
@@ -94,7 +92,7 @@ export function parseArgs(args: string[], arg0: string): [Args, Flags, Error?] {
case 'cd':
case 'chdir':
case 'cwd': // ala bun
- rv.cd = Path.cwd().join(validate_str(value ?? it.next().value))
+ rv.cd = Path.cwd().join(validate.str(value ?? it.next().value))
break
case 'help':
nonovalue()
@@ -151,7 +149,7 @@ export function parseArgs(args: string[], arg0: string): [Args, Flags, Error?] {
flags.verbosity = (flags.verbosity ?? 0) + 1
break
case 'C':
- rv.cd = Path.cwd().join(validate_str(it.next().value))
+ rv.cd = Path.cwd().join(validate.str(it.next().value))
break
case 's':
flags.verbosity = -1;
@@ -209,3 +207,9 @@ function parseBool(input: string) {
return false
}
}
+
+export class UsageError extends Error {
+ constructor(arg: string) {
+ super(`usage error: no such arg: ${arg}`)
+ }
+}
diff --git a/src/hooks/index.ts b/src/hooks/index.ts
index 9941d75ae..c0d1025c4 100644
--- a/src/hooks/index.ts
+++ b/src/hooks/index.ts
@@ -1,46 +1,49 @@
-// order is important to avoid circular dependencies and thus uncaught ReferenceErrors
-import usePrefix from "hooks/usePrefix.ts"
-import useOffLicense from "hooks/useOffLicense.ts"
-import useDownload from "hooks/useDownload.ts"
-import useCache from "hooks/useCache.ts"
-import useCellar from "hooks/useCellar.ts"
-import useExec from "hooks/useExec.ts"
-import useFetch from "hooks/useFetch.ts"
-import useInventory from "hooks/useInventory.ts"
-import useShellEnv from "hooks/useShellEnv.ts"
-import usePantry from "hooks/usePantry.ts"
-import useVirtualEnv from "hooks/useVirtualEnv.ts"
-import usePackageYAML, { usePackageYAMLFrontMatter } from "hooks/usePackageYAML.ts"
-import useSync from "hooks/useSync.ts"
-import useVersion from "hooks/useVersion.ts"
-import useMoustaches from "hooks/useMoustaches.ts"
-import useErrorHandler from "hooks/useErrorHandler.ts"
-import usePrint from "hooks/usePrint.ts"
-import useRun from "hooks/useRun.ts"
-import useConfig, { useEnv } from "hooks/useConfig.ts"
+import useExec from "./useExec.ts"
+import useVirtualEnv from "./useVirtualEnv.ts"
+import usePackageYAML, { usePackageYAMLFrontMatter } from "./usePackageYAML.ts"
+import useVersion from "./useVersion.ts"
+import useErrorHandler, { ExitError } from "./useErrorHandler.ts"
+import usePrint from "./usePrint.ts"
+import useConfig, { Verbosity } from "./useConfig.ts"
+import useLogger from "./useLogger.ts"
+import useRun, { RunError, RunOptions } from "./useRun.ts"
+
+function usePrefix() {
+ return useConfig().prefix
+}
-// but we can sort these alphabetically
export {
- useCache,
- useCellar,
- useDownload,
- useErrorHandler,
useExec,
- useFetch,
- useInventory,
- useMoustaches,
- useOffLicense,
+ useVirtualEnv,
usePackageYAML,
usePackageYAMLFrontMatter,
- usePantry,
- usePrefix,
- useShellEnv,
- useSync,
useVersion,
- useVirtualEnv,
+ useErrorHandler,
usePrint,
- useRun,
+ usePrefix,
useConfig,
- useEnv,
+ useLogger,
+ Verbosity,
+ RunError,
+ useRun,
+ ExitError
+}
+
+export type { RunOptions }
+
+declare global {
+ interface Array {
+ uniq(): Array
+ }
+}
+
+Array.prototype.uniq = function(): Array {
+ const set = new Set()
+ return this.compact(x => {
+ const s = x.toString()
+ if (set.has(s)) return
+ set.add(s)
+ return x
+ })
}
diff --git a/src/hooks/useCache.ts b/src/hooks/useCache.ts
deleted file mode 100644
index 943c5e6ed..000000000
--- a/src/hooks/useCache.ts
+++ /dev/null
@@ -1,31 +0,0 @@
-import { usePrefix } from "hooks"
-import { Package, Stowage } from "types"
-import * as utils from "utils"
-
-export default function useCache() {
- return { path }
-}
-
-type DownloadOptions = {
- type: 'bottle'
- pkg: Package
-} | {
- type: 'src',
- url: URL
- pkg: Package
-}
-
-const path = (stowage: Stowage) => {
- const { pkg, type } = stowage
- const stem = pkg.project.replaceAll("/", "∕")
-
- let filename = `${stem}-${pkg.version}`
- if (type == 'bottle') {
- const { platform, arch } = stowage.host ?? utils.host()
- filename += `+${platform}+${arch}.tar.${stowage.compression}`
- } else {
- filename += stowage.extname
- }
-
- return usePrefix().www.join(filename)
-}
diff --git a/src/hooks/useCellar.ts b/src/hooks/useCellar.ts
deleted file mode 100644
index 7cca59189..000000000
--- a/src/hooks/useCellar.ts
+++ /dev/null
@@ -1,96 +0,0 @@
-import { Package, PackageRequirement, Installation } from "types"
-import { pkg as pkgutils } from "utils"
-import SemVer from "semver"
-import Path from "path"
-//ALERT!! do not usePantry() or you can softlock in usePantry.git.ts
-import { usePrefix, useConfig } from "hooks"
-
-export default function useCellar() {
- return {
- has,
- ls,
- keg,
- resolve,
- shelf,
- }
-}
-
-/// returns the `Installation` if the pkg is installed
-const has = (pkg: Package | PackageRequirement | Path) => resolve(pkg).swallow(/^not-found:/)
-
-/// eg. ~/.tea/deno.land
-const shelf = (project: string) => usePrefix().join(project)
-
-/// eg. ~/.tea/deno.land/v1.2.3
-const keg = (pkg: Package) => shelf(pkg.project).join(`v${pkg.version}`)
-
-/// returns a project’s installations (sorted by version)
-async function ls(project: string) {
- const d = shelf(project)
- const { verbose } = useConfig()
-
- if (!d.isDirectory()) return []
-
- const rv: Installation[] = []
- for await (const [path, {name, isDirectory}] of d.ls()) {
- try {
- if (!isDirectory) continue
- if (!name.startsWith("v") || name == 'var') continue
- const version = new SemVer(name)
- if (await vacant(path)) continue
- rv.push({path, pkg: {project, version}})
- } catch {
- // not console.warn as we allow other dirs as a design choice
- if (verbose) {
- console.warn(`warn: invalid version: ${name}`)
- }
- }
- }
-
- return rv.sort((a, b) => pkgutils.compare(a.pkg, b.pkg))
-}
-
-/// if package is installed, returns its installation
-async function resolve(pkg: Package | PackageRequirement | Path | Installation) {
- const installation = await (async () => {
- if ("pkg" in pkg) { return pkg }
- // ^^ is `Installation`
-
- const prefix = usePrefix()
- if (pkg instanceof Path) {
- const path = pkg
- const version = new SemVer(path.basename())
- const project = path.parent().relative({ to: prefix })
- return {
- path, pkg: { project, version }
- }
- } else if ("version" in pkg) {
- const path = keg(pkg)
- return { path, pkg }
- } else {
- const installations = await ls(pkg.project)
- const versions = installations.map(({ pkg: {version}}) => version)
- const version = pkg.constraint.max(versions)
- console.debug({ installations, versions, version })
- if (version) {
- const path = installations.find(({pkg: {version: v}}) => v.eq(version))!.path
- return { path, pkg: { project: pkg.project, version } }
- }
- }
- throw new Error(`not-found:${pkgutils.str(pkg)}`)
- })()
- if (await vacant(installation.path)) {
- throw new Error(`not-found:${pkgutils.str(installation.pkg)}`)
- }
- return installation
-}
-
-/// if we ignore transient files, is there a package here?
-async function vacant(path: Path): Promise {
- if (!path.isDirectory()) {
- return true
- } else for await (const _ of path.ls()) {
- return false
- }
- return true
-}
diff --git a/src/hooks/useConfig.ts b/src/hooks/useConfig.ts
index c32a43083..e689637da 100644
--- a/src/hooks/useConfig.ts
+++ b/src/hooks/useConfig.ts
@@ -1,98 +1,139 @@
-import Path from "path"
-import { Verbosity } from "types"
+import useVersion from "./useVersion.ts"
+import { Flags } from "../args.ts"
+import { isNumber } from "is-what"
+import { utils, Path } from "tea"
+const { flatmap, panic } = utils
-export interface EnvAccessor {
- getEnvAsObject: () => { [index: string]: string }
-}
-
-export interface Env {
- CI?: string
- CLICOLOR?: string
- CLICOLOR_FORCE?: string
- DEBUG?: string
- GITHUB_ACTIONS?: string
- GITHUB_TOKEN?: string
- NO_COLOR?: string
- PATH?: string
- RUNNER_DEBUG?: string
- SHELL?: string
- SRCROOT?: string
- TEA_DIR?: string
- TEA_FILES?: string
- TEA_FORK_BOMB_PROTECTOR?: string
- TEA_MAGIC?: string
- TEA_PANTRY_PATH?: string
- TEA_PKGS?: string
- TEA_PREFIX?: string
- TEA_REWIND?: string
- VERBOSE?: string
- VERSION?: string
-}
+import useConfig, { Config as ConfigBase, ConfigDefault as ConfigBaseDefault, _internals } from "tea/hooks/useConfig.ts"
-export interface Config {
- isCI: boolean
+export interface Config extends ConfigBase {
+ arg0: Path
- execPath: Path
- loggerGlobalPrefix?: string
- teaPrefix: Path
-
- verbosity: Verbosity
- dryrun: boolean
- keepGoing: boolean
+ logger: {
+ prefix?: string
+ color: boolean
+ }
- verbose: boolean
- debug: boolean
- silent: boolean
+ env: {
+ TEA_DIR?: Path
+ TEA_PKGS?: string
+ TEA_FILES?: string
+ TEA_MAGIC?: string
+ TEA_REWIND?: string
+ TEA_FORK_BOMB_PROTECTOR?: string
+ VERSION?: string
+ SRCROOT?: string
+ SHELL?: string
+ PATH?: string
+
+ obj: Record
+ }
- env: Env
+ modifiers: {
+ dryrun: boolean
+ verbosity: Verbosity
+ json: boolean
+ keepGoing: boolean
+ }
+}
- json: boolean
+export default function(input?: Config): Config {
+ if (!_internals.initialized()) {
+ const rv = useConfig(input ?? panic("useConfig() not initialized")) as Config
+ return rv
+ } else {
+ if (input) console.warn("useConfig() already initialized, new parameters ignored")
+ return useConfig() as Config
+ }
}
-let config: Config | undefined;
+export function ConfigDefault(flags?: Flags, arg0 = Deno.execPath(), defaults?: ConfigBase, env = Deno.env.toObject()): Config {
+ defaults ??= ConfigBaseDefault(env)
+
+ const {
+ TEA_DIR,
+ TEA_REWIND,
+ TEA_FORK_BOMB_PROTECTOR,
+ SHELL,
+ TEA_FILES,
+ TEA_PKGS,
+ TEA_MAGIC,
+ SRCROOT,
+ VERSION,
+ PATH
+ } = env
-// Apply config should only be called once during application initialization
-export function applyConfig(cf: Config) {
- config = cf
- applyVerbosity(config)
+ return {
+ ...defaults,
+ arg0: new Path(arg0),
+ UserAgent: `tea.cli/${useVersion()}`,
+ logger: {
+ prefix: undefined,
+ color: loggerColor(env)
+ },
+ modifiers: {
+ dryrun: flags?.dryrun ?? false,
+ verbosity: flags?.verbosity ?? getVerbosity(env),
+ json: flags?.json ?? false,
+ keepGoing: flags?.keepGoing ?? false,
+ },
+ env: {
+ TEA_DIR: flatmap(TEA_DIR, x => Path.abs(x) ?? Path.cwd().join(x)),
+ TEA_REWIND,
+ TEA_FORK_BOMB_PROTECTOR,
+ SHELL,
+ TEA_FILES,
+ TEA_MAGIC,
+ TEA_PKGS,
+ SRCROOT,
+ VERSION,
+ PATH,
+ obj: env
+ }
+ }
}
-function applyVerbosity(config: Config) {
- function noop() {}
- if (config.verbosity > Verbosity.debug) config.verbosity = Verbosity.debug
- if (config.verbosity < Verbosity.debug) console.debug = noop
- if (config.verbosity < Verbosity.loud) console.verbose = noop
- if (config.verbosity < Verbosity.normal) {
- console.info = noop
- console.warn = noop
- console.log = noop
- console.error = noop
- }
+export enum Verbosity {
+ quiet = -1,
+ normal = 0,
+ loud = 1,
+ debug = 2,
+ trace = 3
}
-// useConfig provides the global configuration state of the application.
-// It must not be called until applyConfig has been called.
-export default function useConfig(): Readonly {
- if (!config) {
- throw Error("contract-violated: config must be applied before it can be used.")
- }
+function getVerbosity(env: Record): Verbosity {
+ const { DEBUG, GITHUB_ACTIONS, RUNNER_DEBUG, VERBOSE } = env
- return config
-}
+ if (DEBUG == '1') return Verbosity.debug
+ if (GITHUB_ACTIONS == 'true' && RUNNER_DEBUG == '1') return Verbosity.debug
-export function useEnv(): Readonly & EnvAccessor {
- const { env } = useConfig()
- return {
- ...env,
- getEnvAsObject: _internals.getEnvAsObject
- }
+ const verbosity = flatmap(VERBOSE, parseInt)
+ return isNumber(verbosity) ? verbosity : Verbosity.normal
}
-const nativeGetEnvAsObject = () => Deno.env.toObject()
+function loggerColor(env: Record) {
+ const isTTY = () => Deno.isatty(Deno.stdout.rid) && Deno.isatty(Deno.stdout.rid)
+
+ if ((env.CLICOLOR ?? '1') != '0' && isTTY()){
+ //https://bixense.com/clicolors/
+ return true
+ }
+ if ((env.CLICOLOR_FORCE ?? '0') != '0') {
+ //https://bixense.com/clicolors/
+ return true
+ }
+ if ((env.NO_COLOR ?? '0') != '0') {
+ return false
+ }
+ if (env.CLICOLOR == '0' || env.CLICOLOR_FORCE == '0') {
+ return false
+ }
+ if (env.CI) {
+ // this is what charm’s lipgloss does, we copy their lead
+ // however surely nobody wants `tea foo > bar` to contain color codes?
+ // the thing is otherwise we have no color in CI since it is not a TTY
+ return true
+ }
-// _internals are used for testing
-export const _internals = {
- getConfig: () => config,
- setConfig: (c: Config) => config = c,
- getEnvAsObject: nativeGetEnvAsObject,
+ return false
}
diff --git a/src/hooks/useDownload.ts b/src/hooks/useDownload.ts
deleted file mode 100644
index 1f780d3d7..000000000
--- a/src/hooks/useDownload.ts
+++ /dev/null
@@ -1,176 +0,0 @@
-import { crypto, toHashString } from "deno/crypto/mod.ts"
-import useLogger, { Logger, teal, gray, logJSON } from "./useLogger.ts"
-import { chuzzle, error, TeaError } from "utils"
-import { usePrefix, useFetch } from "hooks"
-import { isString } from "is_what"
-import Path from "path"
-import useConfig, { useEnv } from "./useConfig.ts"
-
-interface DownloadOptions {
- src: URL
- headers?: Record
- logger?: Logger | string
- dst?: Path
-}
-
-interface RV {
- path: Path
-
- // we only give you the sha if we download
- // if we found the cache then you have to calculate the sha yourself
- sha: string | undefined
-}
-
-async function internal({ src, headers, logger, dst }: DownloadOptions): Promise<[Path, ReadableStream | undefined]>
-{
- const { isCI, silent, json } = useConfig();
- const { GITHUB_TOKEN } = useEnv();
- logger = isString(logger) ? useLogger(logger) : logger ?? useLogger()
-
- const hash = hash_key(src)
- const mtime_entry = hash.join("mtime")
- const etag_entry = hash.join("etag")
-
- dst ??= hash.join(src.path().basename())
- if (src.protocol === "file:") throw new Error()
-
- console.verbose({src: src, dst})
-
- if (dst.isReadableFile()) {
- headers ??= {}
- if (etag_entry.isFile()) {
- headers["If-None-Match"] = await etag_entry.read()
- }
- // sending both if we have them is ChatGPT recommended
- // also this fixes getting the mysql.com sources, otherwise it redownloads 400MB every time!
- if (mtime_entry.isFile()) {
- headers["If-Modified-Since"] = await mtime_entry.read()
- }
- } else if (!json) {
- logger.replace(teal('downloading'))
- }
-
- // so the user can add private repos if they need to etc.
- if (/(^|\.)github.com$/.test(src.host)) {
- if (GITHUB_TOKEN) {
- headers ??= {}
- headers["Authorization"] = `bearer ${GITHUB_TOKEN}`
- }
- }
-
- const rsp = await useFetch(src, { headers })
-
- switch (rsp.status) {
- case 200: {
- const sz = chuzzle(parseInt(rsp.headers.get("Content-Length")!))
-
- let txt = teal('downloading')
- if (sz) txt += ` ${gray(pretty_size(sz))}`
- if (!json) {
- logger.replace(txt)
- } else {
- logJSON({status: "downloading"})
- }
-
- const reader = rsp.body ?? error.panic()
-
- const text = rsp.headers.get("Last-Modified")
- if (text) mtime_entry.write({text, force: true})
- const etag = rsp.headers.get("ETag")
- if (etag) etag_entry.write({text: etag, force: true})
-
- if (isCI || silent) {
- return [dst, reader]
- } else {
- let n = 0
- return [dst, reader.pipeThrough(new TransformStream({
- transform: (buf, controller) => {
- n += buf.length
- if (json) {
- logJSON({status: "downloading", "received": n, "content-size": sz })
- } else if (!sz) {
- (logger as Logger).replace(`${txt} ${pretty_size(n)}`)
- } else {
- let s = txt
- if (n < sz) {
- let pc = n / sz * 100;
- pc = pc < 1 ? Math.round(pc) : Math.floor(pc); // don’t say 100% at 99.5%
- s += ` ${pc}%`
- } else {
- s = teal('extracting')
- }
- (logger as Logger).replace(s)
- }
- controller.enqueue(buf)
- }}))]
- }
- }
- case 304:
- if (json) {
- logJSON({status: "downloaded"})
- } else {
- logger.replace(`cache: ${teal('hit')}`)
- }
- return [dst, undefined]
- default:
- throw new Error(`${rsp.status}: ${src}`)
- }
-}
-
-async function download(opts: DownloadOptions): Promise {
- try {
- const [path, stream] = await internal(opts)
- if (!stream) return path // already downloaded
-
- path.parent().mkpath()
- const f = await Deno.open(path.string, {create: true, write: true, truncate: true})
- await stream.pipeTo(f.writable)
- return path
- } catch (cause) {
- throw new TeaError('http', {cause, ...opts})
- }
-}
-
-async function stream(opts: DownloadOptions): Promise | undefined> {
- try {
- const [, stream] = await internal(opts)
- return stream
- } catch (cause) {
- throw new TeaError('http', {cause, ...opts})
- }
-}
-
-function hash_key(url: URL): Path {
- function hash(url: URL) {
- const formatted = `${url.pathname}${url.search ? "?" + url.search : ""}`
- const contents = new TextEncoder().encode(formatted)
- return toHashString(crypto.subtle.digestSync("SHA-256", contents))
- }
-
- const prefix = usePrefix().www
-
- return prefix
- .join(url.protocol.slice(0, -1))
- .join(url.hostname)
- .join(hash(url))
- .mkpath()
-}
-
-export default function useDownload() {
- return {
- download,
- stream,
- hash_key
- }
-}
-
-function pretty_size(n: number) {
- const units = ["B", "KiB", "MiB", "GiB", "TiB"]
- let i = 0
- while (n > 1024 && i < units.length - 1) {
- n /= 1024
- i++
- }
- const precision = n < 10 ? 2 : n < 100 ? 1 : 0
- return `${n.toFixed(precision)} ${units[i]}`
-}
diff --git a/src/hooks/useErrorHandler.ts b/src/hooks/useErrorHandler.ts
index a53dd4b12..9f084f405 100644
--- a/src/hooks/useErrorHandler.ts
+++ b/src/hooks/useErrorHandler.ts
@@ -1,39 +1,25 @@
-import * as logger from "./useLogger.ts"
-import { usePantry, usePrefix, useInventory } from "hooks"
-import useConfigBase, { applyConfig } from "hooks/useConfig.ts"
-import { chuzzle, TeaError, undent } from "utils"
-import Path from "path"
-import { ExitError, Verbosity } from "../types.ts"
-
-const useConfig = () => {
- try {
- return useConfigBase()
- } catch {
- // lol we threw before we could apply the config
- // these defaults will do for error handling
- applyConfig({
- isCI: false,
- execPath: Path.root,
- teaPrefix: Path.root,
- verbosity: Verbosity.normal,
- dryrun: false,
- keepGoing: false,
- verbose: false,
- debug: false,
- silent: false,
- env: {},
- json: false
- })
- return useConfigBase()
+import { useLogger, Verbosity, useConfig } from "hooks"
+import { hooks, Path, TeaError } from "tea"
+const { usePantry, useInventory } = hooks
+import undent from "outdent"
+
+// ExitError will cause the application to exit with the specified exit code if it bubbles
+// up to the main error handler
+export class ExitError extends Error {
+ code: number
+ constructor(code: number) {
+ super(`exiting with code: ${code}`)
+ this.code = code
}
}
export async function suggestions(err: TeaError) {
+ const { teal } = useLogger()
switch (err.id) {
case 'not-found: pantry: package.yml': {
const suggestion = await getClosestPackageSuggestion(err.ctx.project).swallow()
return suggestion
- ? `did you mean \`${logger.teal(suggestion)}\`? otherwise… see you on GitHub?`
+ ? `did you mean \`${teal(suggestion)}\`? otherwise… see you on GitHub?`
: undefined
}
case 'not-found: pkg.version':
@@ -46,17 +32,20 @@ export async function suggestions(err: TeaError) {
}
export default async function(err: Error) {
- const { silent, debug, json } = useConfig()
+ const { logJSON, red, gray } = useLogger()
+ const { verbosity, json } = useConfig().modifiers
+ const silent = verbosity <= Verbosity.quiet
+ const debug = verbosity >= Verbosity.debug
if (err instanceof ExitError) {
- if (json) logger.logJSON({ error: true })
+ if (json) logJSON({ error: true })
return err.code
} else if (err instanceof TeaError) {
if (json) {
- logger.logJSON({ error: true, message: msg(err) })
+ logJSON({ error: true, message: msg(err) })
} else if (!silent) {
const suggestion = await suggestions(err).swallow()
- console.error(`${logger.red('error')}: ${err.title()} (${logger.gray(err.code())})`)
+ console.error(`${red('error')}: ${err.title()} (${gray(err.code())})`)
if (suggestion) {
console.error()
console.error(suggestion)
@@ -65,10 +54,10 @@ export default async function(err: Error) {
console.error(msg(err))
if (debug) console.error(err.ctx)
}
- const code = chuzzle(parseInt(err.code().match(/\d+$/)?.[0] ?? '1')) ?? 1
+ const code = parseInt(err.code().match(/\d+$/)?.[0] ?? '1').chuzzle() ?? 1
return code
} else if (json) {
- logger.logJSON({ error: true, message: err.message })
+ logJSON({ error: true, message: err.message })
} else if (!silent) {
const { stack, message } = err ?? {}
@@ -76,12 +65,12 @@ export default async function(err: Error) {
const url = `https://github.com/teaxyz/cli/issues/new?title=${title}`
console.error()
- console.error(`${logger.red("panic")}:`, "spilt tea. we’re sorry and we’ll fix it… but you have to report the bug!")
+ console.error(`${red("panic")}:`, "spilt tea. we’re sorry and we’ll fix it… but you have to report the bug!")
console.error()
- console.error(" ", logger.gray(url))
+ console.error(" ", gray(url))
console.error()
console.error("----------------------------------------------------->> attachment begin")
- console.error(logger.gray(stack ?? "null"))
+ console.error(gray(stack ?? "null"))
console.debug("------------------------------------------------------------------------")
console.debug({ err })
console.error("<<----------------------------------------------------- attachment end")
@@ -97,12 +86,13 @@ export default async function(err: Error) {
/// this is here because error.ts cannot import higher level modules
/// like hooks without creating a cyclic dependency
function msg(err: TeaError): string {
+ const { gray } = useLogger()
let msg = err.message
const { ctx } = err
switch (err.code()) {
case 'spilt-tea-009':
- if (ctx.filename instanceof Path && !ctx.filename.in(usePrefix())) {
+ if (ctx.filename instanceof Path && !ctx.filename.string.startsWith(useConfig().prefix.string)) {
// this yaml is being worked on by the user
msg = `${ctx.filename.prettyLocalString()}: ${ctx.cause?.message ?? 'unknown cause'}`
} else {
@@ -113,7 +103,7 @@ function msg(err: TeaError): string {
https://github.com/teaxyz/pantry/issues/new
----------------------------------------------------->> attachment begin
- ${logger.gray(attachment)}
+ ${gray(attachment)}
<<------------------------------------------------------- attachment end
`
}
@@ -129,7 +119,7 @@ async function getClosestPackageSuggestion(input: string) {
for await (const {project} of pantry.ls()) {
if (min == 0) break
- pantry.getProvides({ project }).then(provides => {
+ pantry.project(project).provides().then(provides => {
if (provides.includes(input)) {
choice = project
min = 0
diff --git a/src/hooks/useExec.ts b/src/hooks/useExec.ts
index 75a68385a..98d28fc38 100644
--- a/src/hooks/useExec.ts
+++ b/src/hooks/useExec.ts
@@ -1,12 +1,13 @@
-import { usePantry, useShellEnv, useDownload, usePackageYAMLFrontMatter, usePrefix } from "hooks"
-import { PackageSpecification, Installation, PackageRequirement } from "types"
-import { hydrate, resolve, install as base_install, link } from "prefab"
+import { prefab, utils, hooks, PackageSpecification, Installation, PackageRequirement, Path, semver, TeaError } from "tea"
+import { usePackageYAMLFrontMatter } from "./usePackageYAML.ts"
+import { ExitError } from "./useErrorHandler.ts"
import { VirtualEnv } from "./useVirtualEnv.ts"
-import { flatten } from "./useShellEnv.ts"
-import useLogger, { logJSON } from "./useLogger.ts"
-import { pkg as pkgutils, TeaError } from "utils"
-import * as semver from "semver"
-import Path from "path"
+import useConfig from "./useConfig.ts"
+import useLogger from "./useLogger.ts"
+import undent from "outdent"
+
+const { usePantry, useCellar, useDownload, useShellEnv } = hooks
+const { hydrate, resolve, install: base_install, link } = prefab
interface Parameters {
args: string[]
@@ -22,6 +23,7 @@ export default async function({ pkgs, inject, sync, ...opts }: Parameters) {
if (arg0) cmd[0] = arg0?.toString() // if we downloaded it then we need to replace args[0]
const clutch = pkgs.length > 0
const env: Record = inject?.env ?? {}
+ const sh = useShellEnv()
if (inject) {
const {version, srcroot, teafiles, ...vrtenv} = inject
@@ -54,7 +56,7 @@ export default async function({ pkgs, inject, sync, ...opts }: Parameters) {
}
if (precmd.length == 0) {
- const found = await usePantry().getInterpreter(arg0.extname())
+ const found = await usePantry().which({ interprets: arg0.extname() })
if (found) {
pkgs.push({ ...found, constraint: new semver.Range('*') })
precmd.unshift(...found.args)
@@ -97,14 +99,14 @@ export default async function({ pkgs, inject, sync, ...opts }: Parameters) {
pkgs = dry // reassign as condensed + sorted
}
- Object.assign(env, flatten(await useShellEnv({ installations })))
+ Object.assign(env, sh.flatten(await sh.map({ installations })))
- env["TEA_PREFIX"] ??= usePrefix().string
+ env["TEA_PREFIX"] ??= useConfig().prefix.string
return { env, cmd, installations, pkgs }
async function add_companions(pkg: {project: string}) {
- pkgs.push(...await usePantry().getCompanions(pkg))
+ pkgs.push(...await usePantry().project(pkg).companions())
}
}
@@ -112,8 +114,9 @@ export default async function({ pkgs, inject, sync, ...opts }: Parameters) {
///////////////////////////////////////////////////////////////////////////// funcs
async function install(pkgs: PackageSpecification[], update: boolean) {
- const { json } = useConfig()
- const logger = useLogger()
+ const { modifiers: { json, dryrun }, env } = useConfig()
+ const logger = useLogger().new()
+ const { logJSON } = useLogger()
if (!json) {
logger.replace("resolving package graph")
@@ -128,10 +131,30 @@ async function install(pkgs: PackageSpecification[], update: boolean) {
logger.clear()
if (json) {
- logJSON({ status: "resolved", pkgs: pending.map(pkgutils.str) })
+ logJSON({ status: "resolved", pkgs: pending.map(utils.pkg.str) })
}
- for (const pkg of pending) {
+ if (!dryrun && env.TEA_MAGIC?.split(':').includes("prompt")) {
+ if (!Deno.isatty(Deno.stdin.rid)) {
+ throw new Error("TEA_MAGIC=prompt but stdin is not a tty")
+ }
+
+ do {
+ const val = prompt(undent`
+ ┌ ⚠️ tea requests to install: ${pending.map(utils.pkg.str).join(", ")}
+ └ \x1B[1mallow?\x1B[0m [y/n]`
+ )?.toLowerCase()
+
+ if (val === "y") {
+ break
+ }
+ if (val === "n") {
+ throw new ExitError(1)
+ }
+ } while (true)
+ }
+
+ if (!dryrun) for (const pkg of pending) {
const install = await base_install(pkg)
await link(install)
installed.push(install)
@@ -162,9 +185,6 @@ async function read_shebang(path: Path): Promise {
return []
}
-import useCellar from "./useCellar.ts"
-import useConfig, { useEnv } from "./useConfig.ts"
-
async function fetch_it(arg0: string | undefined) {
if (!arg0) return
@@ -174,7 +194,7 @@ async function fetch_it(arg0: string | undefined) {
return path.chmod(0o700) //FIXME like… I don’t feel we should necessarily do this…
}
- const { execPath } = useConfig()
+ const { arg0: execPath } = useConfig()
const path = Path.cwd().join(arg0)
if (path.exists() && execPath.basename() == "tea") {
// ^^ in the situation where we are shadowing other tool names
@@ -220,12 +240,12 @@ type WhichResult = PackageRequirement & {
}
export async function which(arg0: string | undefined) {
- if (!arg0?.chuzzle() || arg0.includes("/")) {
+ if (!arg0?.trim() || arg0.includes("/")) {
// no shell we know allows searching for subdirectories off PATH
return false
}
- const { TEA_PKGS, TEA_MAGIC } = useEnv()
+ const { TEA_PKGS, TEA_MAGIC } = useConfig().env
const pantry = usePantry()
let found: { project: string, constraint: semver.Range, shebang: string[] } | undefined
const promises: Promise[] = []
@@ -233,13 +253,13 @@ export async function which(arg0: string | undefined) {
for await (const entry of pantry.ls()) {
if (found) break
- const p = pantry.getProvides(entry).then(providers => {
+ const p = pantry.project(entry).provides().then(providers => {
for (const provider of providers) {
if (found) {
return
} else if (provider == arg0 || (!abracadabra && provider == `tea-${arg0}`)) {
const inenv = TEA_PKGS?.split(":")
- .map(pkgutils.parse)
+ .map(utils.pkg.parse)
.find(x => x.project == entry.project)
if (inenv) {
// we are being executed via the command not found handler inside a dev-env
@@ -282,7 +302,7 @@ export async function which(arg0: string | undefined) {
}).swallow(/^parser: pantry: package.yml/)
promises.push(p)
- const pp = pantry.getProvider({ project: entry.project }).then(f => {
+ const pp = pantry.project(entry).provider().then(f => {
if (!f) return
const rv = f(arg0)
if (rv) found = {
diff --git a/src/hooks/useFetch.ts b/src/hooks/useFetch.ts
deleted file mode 100644
index 447fc41e8..000000000
--- a/src/hooks/useFetch.ts
+++ /dev/null
@@ -1,8 +0,0 @@
-import { useVersion } from "hooks";
-
-// useFetch wraps the native Deno fetch api and inserts a User-Agent header
-export default function useFetch(input: string | URL | Request, init?: RequestInit | undefined): Promise {
- const requestInit = init ?? {} as RequestInit
- requestInit.headers = { ...requestInit.headers, "User-Agent": `tea.cli/${useVersion()}` }
- return fetch(input, requestInit)
-}
diff --git a/src/hooks/useInventory.ts b/src/hooks/useInventory.ts
deleted file mode 100644
index baff8206c..000000000
--- a/src/hooks/useInventory.ts
+++ /dev/null
@@ -1,59 +0,0 @@
-import { Package, PackageRequirement } from "types"
-import { host, error, TeaError } from "utils"
-import SemVer from "semver"
-import Path from "../vendor/Path.ts"
-import { useFetch } from "hooks";
-
-export interface Inventory {
- [project: string]: {
- [platform: string]: {
- [arch: string]: string[]
- }
- }
-}
-
-const select = async (rq: PackageRequirement | Package) => {
- const versions = await get(rq)
-
- console.debug({ project: rq.project, versions })
-
- if ("constraint" in rq) {
- return rq.constraint.max(versions)
- } else if (versions.find(x => x.eq(rq.version))) {
- return rq.version
- }
-}
-
-const get = async (rq: PackageRequirement | Package) => {
- const { platform, arch } = host()
-
- const url = new URL('https://dist.tea.xyz')
- url.pathname = Path.root.join(rq.project, platform, arch, 'versions.txt').string
-
- const rsp = await useFetch(url)
-
- if (!rsp.ok) {
- const cause = new Error(`${rsp.status}: ${url}`)
- throw new TeaError('http', {cause})
- }
-
- const releases = await rsp.text()
- let versions = releases.split("\n").compact(x => new SemVer(x))
-
- if (versions.length < 1) throw new Error()
-
- if (rq.project == 'openssl.org') {
- // workaround our previous sins
- const v = new SemVer("1.1.118")
- versions = versions.filter(x => x.neq(v))
- }
-
- return versions
-}
-
-export default function useInventory() {
- return {
- select: error.wrap(select, 'http'),
- get
- }
-}
diff --git a/src/hooks/useLogger.ts b/src/hooks/useLogger.ts
index a15941b30..15231c9d1 100644
--- a/src/hooks/useLogger.ts
+++ b/src/hooks/useLogger.ts
@@ -1,7 +1,7 @@
+import useConfig, { Verbosity } from "./useConfig.ts"
import { colors, tty } from "cliffy/ansi/mod.ts"
-import { flatmap } from "utils";
-import useConfig, { useEnv } from "hooks/useConfig.ts";
-import { Verbosity } from "types";
+import { utils } from "tea"
+const { flatmap } = utils
// ref https://github.com/chalk/ansi-regex/blob/main/index.js
const ansi_escapes_rx = new RegExp([
@@ -26,42 +26,21 @@ function ln(s: string, prefix_length: number) {
}
}
-export default function useLogger(prefix?: string) {
- const { verbosity, loggerGlobalPrefix } = useConfig()
- return new Logger(verbosity, prefix, loggerGlobalPrefix)
+export default function() {
+ return {
+ new: useLogger,
+ teal, red, gray, dark, lite,
+ logJSON
+ }
}
-function colorIfTTY(x: string, colorMethod: (x: string)=>string) {
- //TODO this function needs to take a out/err parameter since that's what
- // needs to be tested rather than testing for both (this is safe for now)
- const isTTY = () => Deno.isatty(Deno.stdout.rid) && Deno.isatty(Deno.stdout.rid)
- const { isCI } = useConfig()
- const { CLICOLOR, CLICOLOR_FORCE, NO_COLOR } = useEnv()
-
- const color_on = () => {
- if ((CLICOLOR ?? '1') != '0' && isTTY()){
- //https://bixense.com/clicolors/
- return true
- }
- if ((CLICOLOR_FORCE ?? '0') != '0') {
- //https://bixense.com/clicolors/
- return true
- }
- if ((NO_COLOR ?? '0') != '0') {
- return false
- }
- if (CLICOLOR == '0' || CLICOLOR_FORCE == '0') {
- return false
- }
- if (isCI) {
- // this is what charm’s lipgloss does, we copy their lead
- // however surely nobody wants `tea foo > bar` to contain color codes?
- // the thing is otherwise we have no color in CI since it is not a TTY
- return true
- }
- }
+function useLogger(prefix?: string) {
+ const { modifiers: { verbosity }, logger: { prefix: loggerGlobalPrefix, color } } = useConfig()
+ return new Logger(verbosity, prefix, loggerGlobalPrefix, color)
+}
- return color_on() ? colorMethod(x) : x
+function colorIfTTY(x: string, colorMethod: (x: string)=>string) {
+ return useConfig().logger.color ? colorMethod(x) : x
}
export const teal = (x: string) => colorIfTTY(x, (x) => colors.rgb8(x, 86))
@@ -74,13 +53,15 @@ export class Logger {
readonly verbosity: Verbosity
readonly prefix: string
readonly globalPrefix?: string
+ readonly sequences_ok: boolean
lines = 0
last_line = ''
tty = tty({ stdout: Deno.stderr })
prefix_length: number
- constructor(verbosity: Verbosity, prefix?: string, globalPrefix?: string) {
+ constructor(verbosity: Verbosity, prefix: string | undefined, globalPrefix: string | undefined, sequences_ok: boolean) {
this.verbosity = verbosity
+ this.sequences_ok = sequences_ok
prefix = prefix?.chuzzle()
this.prefix_length = prefix?.length ?? 0
@@ -103,7 +84,7 @@ export class Logger {
if (this.lines) {
const n = ln(this.last_line, this.prefix_length)
- if (this.verbosity < 1) {
+ if (this.sequences_ok) {
this.tty.cursorLeft.cursorUp(n).eraseDown()
}
this.lines -= n
@@ -119,7 +100,7 @@ export class Logger {
}
clear() {
- if (this.lines && this.verbosity >= 0) {
+ if (this.sequences_ok && this.lines && this.verbosity >= 0) {
this.tty.cursorLeft.cursorUp(this.lines).eraseDown(this.lines)
this.lines = 0
}
diff --git a/src/hooks/useMoustaches.ts b/src/hooks/useMoustaches.ts
deleted file mode 100644
index b1d5d9dd0..000000000
--- a/src/hooks/useMoustaches.ts
+++ /dev/null
@@ -1,45 +0,0 @@
-import SemVer from "semver"
-import { host } from "utils"
-
-export default function useMoustaches() {
- return {
- apply,
- tokenize: {
- version: tokenizeVersion,
- host: tokenizeHost
- }
- }
-}
-
-function tokenizeVersion(version: SemVer, prefix = 'version') {
- const rv = [
- { from: prefix, to: `${version}` },
- { from: `${prefix}.major`, to: `${version.major}` },
- { from: `${prefix}.minor`, to: `${version.minor}` },
- { from: `${prefix}.patch`, to: `${version.patch}` },
- { from: `${prefix}.marketing`, to: `${version.major}.${version.minor}` },
- { from: `${prefix}.build`, to: version.build.join('+') },
- { from: `${prefix}.raw`, to: version.raw },
- ]
- if ('tag' in version) {
- rv.push({from: `${prefix}.tag`, to: (version as unknown as {tag: string}).tag})
- }
- return rv
-}
-
-//TODO replace `hw` with `host`
-function tokenizeHost() {
- const { arch, target, platform } = host()
- return [
- { from: "hw.arch", to: arch },
- { from: "hw.target", to: target },
- { from: "hw.platform", to: platform },
- { from: "hw.concurrency", to: navigator.hardwareConcurrency.toString() }
- ]
-}
-
-function apply(input: string, map: { from: string, to: string }[]) {
- return map.reduce((acc, {from, to}) =>
- acc.replace(new RegExp(`(^\\$)?{{\\s*${from}\\s*}}`, "g"), to),
- input)
-}
diff --git a/src/hooks/useOffLicense.ts b/src/hooks/useOffLicense.ts
deleted file mode 100644
index 4eb385098..000000000
--- a/src/hooks/useOffLicense.ts
+++ /dev/null
@@ -1,28 +0,0 @@
-import { Stowage } from "types"
-import { host } from "utils"
-import Path from "path"
-
-type Type = 's3'
-
-export default function useOffLicense(_type: Type) {
- return { url, key }
-}
-
-function key(stowage: Stowage) {
- let rv = Path.root.join(stowage.pkg.project)
- if (stowage.type == 'bottle') {
- const { platform, arch } = stowage.host ?? host()
- rv = rv.join(`${platform}/${arch}`)
- }
- let fn = `v${stowage.pkg.version}`
- if (stowage.type == 'bottle') {
- fn += `.tar.${stowage.compression}`
- } else {
- fn += stowage.extname
- }
- return rv.join(fn).string.slice(1)
-}
-
-function url(stowage: Stowage) {
- return new URL(`https://dist.tea.xyz/${key(stowage)}`)
-}
diff --git a/src/hooks/usePackageYAML.ts b/src/hooks/usePackageYAML.ts
index 62a7364e3..24ca26644 100644
--- a/src/hooks/usePackageYAML.ts
+++ b/src/hooks/usePackageYAML.ts
@@ -1,10 +1,8 @@
-import { PackageRequirement } from "types"
-import { isPlainObject, isString, isArray, PlainObject } from "is_what"
-import { validatePackageRequirement } from "utils/hacks.ts"
-import { usePrefix } from "hooks"
-import { validate_plain_obj } from "utils"
-import Path from "path"
-import useMoustaches from "./useMoustaches.ts"
+import { isPlainObject, isString, isArray, PlainObject } from "is-what"
+import { PackageRequirement, Path, hacks, utils, hooks } from "tea"
+const { validatePackageRequirement } = hacks
+const { useMoustaches, useConfig } = hooks
+const { validate } = utils
interface Return1 {
getDeps: (wbuild: boolean) => PackageRequirement[]
@@ -21,8 +19,8 @@ export default function usePackageYAML(yaml: unknown): Return1 {
// deno-lint-ignore no-explicit-any
function go(node: any) {
if (!node) return []
- return Object.entries(validate_plain_obj(node))
- .compact(([project, constraint]) => validatePackageRequirement({ project, constraint }))
+ return Object.entries(validate.obj(node))
+ .compact(([project, constraint]) => validatePackageRequirement(project, constraint))
}
}
@@ -67,7 +65,7 @@ export function refineFrontMatter(obj: unknown, srcroot?: Path): FrontMatter {
const foo = [
...moustaches.tokenize.host(),
- { from: "tea.prefix", to: usePrefix().string },
+ { from: "tea.prefix", to: useConfig().prefix.string },
{ from: "home", to: Path.home().string }
]
diff --git a/src/hooks/usePantry.ts b/src/hooks/usePantry.ts
deleted file mode 100644
index 40cba55eb..000000000
--- a/src/hooks/usePantry.ts
+++ /dev/null
@@ -1,331 +0,0 @@
-import { Package, PackageRequirement, Installation } from "types"
-import { host, validate_plain_obj, pkg, TeaError, validate_arr } from "utils"
-import { isNumber, isPlainObject, isString, isArray, isPrimitive, PlainObject, isBoolean } from "is_what"
-import { validatePackageRequirement } from "utils/hacks.ts"
-import { useCellar, usePrefix } from "hooks"
-import Path from "path"
-
-interface Entry {
- dir: Path
- yml: () => Promise
- versions: Path
-}
-
-export interface Interpreter {
- project: string // FIXME: should probably be a stronger type
- args: string[]
-}
-
-export default function usePantry() {
- return {
- getDeps,
- getCompanions,
- getProvides,
- getProvider,
- getInterpreter,
- getRuntimeEnvironment,
- available,
- ls,
- prefix: getPantryPrefix()
- }
-}
-
-/// returns ONE LEVEL of deps, to recurse use `hydrate.ts`
-const getDeps = async (pkg: Package | PackageRequirement) => {
- const yml = await entry(pkg).yml()
- return parse_pkgs_node(yml.dependencies)
-}
-
-// deno-lint-ignore no-explicit-any
-function parse_pkgs_node(node: any) {
- if (!node) return []
- node = validate_plain_obj(node)
- platform_reduce(node)
-
- const rv: PackageRequirement[] = []
- for (const [project, constraint] of Object.entries(node)) {
- rv.compact_push(validatePackageRequirement({ project, constraint }))
- }
- return rv
-}
-
-const getProvides = async (pkg: { project: string }) => {
- const yml = await entry(pkg).yml()
- let node = yml["provides"]
- if (!node) return []
- if (isPlainObject(node)) {
- node = node[host().platform]
- }
- if (!isArray(node)) throw new Error("bad-yaml")
-
- return node.compact(x => {
- if (isPlainObject(x)) {
- x = x["executable"]
- }
- if (isString(x)) {
- if (x.startsWith("bin/")) return x.slice(4)
- if (x.startsWith("sbin/")) return x.slice(5)
- }
- })
-}
-
-import { parse as parseYaml } from "deno/encoding/yaml.ts"
-
-const getProvider = async ({ project }: { project: string }) => {
- for (const prefix of pantry_paths()) {
- if (!prefix.exists()) continue
- const dir = prefix.join(project)
- const filename = dir.join("provider.yml")
- if (!filename.exists()) continue
- const read = await Deno.readTextFile(filename.string)
- const yaml = validate_plain_obj(await parseYaml(read))
- const cmds = validate_arr(yaml.cmds)
- return (binname: string) => {
- if (!cmds.includes(binname)) return
- const args = yaml['args']
- if (isPlainObject(args)) {
- if (args[binname]) {
- return get_args(args[binname])
- } else {
- return get_args(args['...'])
- }
- } else {
- return get_args(args)
- }
- }
- }
-
- function get_args(input: unknown) {
- if (isString(input)) {
- return input.split(/\s+/)
- } else {
- return validate_arr(input)
- }
- }
-}
-
-const getCompanions = async (pkg: {project: string}) => {
- const yml = await entry(pkg).yml()
- const node = yml["companions"]
- return parse_pkgs_node(node)
-}
-
-const getInterpreter = async (extension: string): Promise => {
- extension = extension.slice(1)
- if (!extension) return
- for await (const pkg of ls()) {
- const yml = await entry(pkg).yml()
- const node = yml["interprets"]
- if (!isPlainObject(node)) continue
- try {
- const { extensions, args } = yml["interprets"]
- if ((isString(extensions) && extensions === extension) ||
- (isArray(extensions) && extensions.includes(extension))) {
- return { project: pkg.project, args: isArray(args) ? args : [args] }
- }
- } catch {
- continue
- }
- }
- return undefined
-}
-
-const getRuntimeEnvironment = async (pkg: Package): Promise> => {
- const yml = await entry(pkg).yml()
- const obj = validate_plain_obj(yml["runtime"]?.["env"] ?? {})
- return expand_env_obj(obj, pkg, [])
-}
-
-const available = async (pkg: PackageRequirement): Promise => {
- let { platforms } = await entry(pkg).yml()
- if (!platforms) return true
- if (isString(platforms)) platforms = [platforms]
- if (!isArray(platforms)) throw new Error("bad-yaml")
- return platforms.includes(host().platform) ||platforms.includes(`${host().platform}/${host().arch}`)
-}
-
-function entry({ project }: { project: string }): Entry {
- for (const prefix of pantry_paths()) {
- if (!prefix.exists()) throw new TeaError('not-found: pantry', { path: prefix.parent() })
- const dir = prefix.join(project)
- const filename = dir.join("package.yml")
- if (!filename.exists()) continue
- const yml = async () => {
- try {
- const yml = await filename.readYAML()
- if (!isPlainObject(yml)) throw null
- return yml
- } catch (cause) {
- throw new TeaError('parser: pantry: package.yml', {cause, project, filename})
- }
- }
- const versions = dir.join("versions.txt")
- return { dir, yml, versions }
- }
-
- throw new TeaError('not-found: pantry: package.yml', {project}, )
-}
-
-/// expands platform specific keys into the object
-/// expands inplace because JS is nuts and you have to suck it up
-function platform_reduce(env: PlainObject) {
- const sys = host()
- for (const [key, value] of Object.entries(env)) {
- const [os, arch] = (() => {
- let match = key.match(/^(darwin|linux)\/(aarch64|x86-64)$/)
- if (match) return [match[1], match[2]]
- if ((match = key.match(/^(darwin|linux)$/))) return [match[1]]
- if ((match = key.match(/^(aarch64|x86-64)$/))) return [,match[1]]
- return []
- })()
-
- if (!os && !arch) continue
- delete env[key]
- if (os && os != sys.platform) continue
- if (arch && arch != sys.arch) continue
-
- const dict = validate_plain_obj(value)
- for (const [key, value] of Object.entries(dict)) {
- // if user specifies an array then we assume we are supplementing
- // otherwise we are replacing. If this is too magical let us know
- if (isArray(value)) {
- if (!env[key]) env[key] = []
- else if (!isArray(env[key])) env[key] = [env[key]]
- //TODO if all-platforms version comes after the specific then order accordingly
- env[key].push(...value)
- } else {
- env[key] = value
- }
- }
- }
-}
-
-function expand_env_obj(env_: PlainObject, pkg: Package, deps: Installation[]): Record {
- const env = {...env_}
-
- platform_reduce(env)
-
- const rv: Record = {}
-
- for (let [key, value] of Object.entries(env)) {
- if (isArray(value)) {
- value = value.map(transform).join(" ")
- } else {
- value = transform(value)
- }
-
- rv[key] = value
- }
-
- return rv
-
- // deno-lint-ignore no-explicit-any
- function transform(value: any): string {
- if (!isPrimitive(value)) throw new Error(`invalid-env-value: ${JSON.stringify(value)}`)
-
- if (isBoolean(value)) {
- return value ? "1" : "0"
- } else if (value === undefined || value === null) {
- return "0"
- } else if (isString(value)) {
- const mm = useMoustaches()
- const home = Path.home().string
- const obj = [
- { from: 'env.HOME', to: home }, // historic, should be removed at v1
- { from: 'home', to: home } // remove, stick with just ~
- ]
- obj.push(...mm.tokenize.all(pkg, deps))
- return mm.apply(value, obj)
- } else if (isNumber(value)) {
- return value.toString()
- }
- throw new Error("unexpected-error")
- }
-}
-
-//////////////////////////////////////////// useMoustaches() additions
-import useMoustachesBase from "./useMoustaches.ts"
-import { useEnv } from "./useConfig.ts"
-
-function useMoustaches() {
- const base = useMoustachesBase()
-
- const deps = (deps: Installation[]) => {
- const map: {from: string, to: string}[] = []
- for (const dep of deps ?? []) {
- map.push({ from: `deps.${dep.pkg.project}.prefix`, to: dep.path.string })
- map.push(...useMoustaches().tokenize.version(dep.pkg.version, `deps.${dep.pkg.project}.version`))
- }
- return map
- }
-
- const tea = () => [{ from: "tea.prefix", to: usePrefix().string }]
-
- const all = (pkg: Package, deps_: Installation[]) => [
- ...deps(deps_),
- ...tokenizePackage(pkg),
- ...tea(),
- ...base.tokenize.version(pkg.version),
- ...base.tokenize.host(),
- ]
-
- return {
- apply: base.apply,
- tokenize: {
- ...base.tokenize,
- deps, pkg, tea, all
- }
- }
-}
-
-function tokenizePackage(pkg: Package) {
- return [{ from: "prefix", to: useCellar().keg(pkg).string }]
-}
-
-const getPantryPrefix = () => usePrefix().join('tea.xyz/var/pantry/projects')
-
-export function pantry_paths(): Path[] {
- const rv: Path[] = []
- const { TEA_PANTRY_PATH } = useEnv()
-
- const prefix = getPantryPrefix()
- if (prefix.isDirectory()) rv.push(prefix)
-
- if (TEA_PANTRY_PATH) for (const path of TEA_PANTRY_PATH.split(":").reverse()) {
- rv.unshift(Path.cwd().join(path, "projects"))
- }
-
- if (rv.length == 0) throw new TeaError("not-found: pantry", {prefix})
-
- return rv
-}
-
-interface LsEntry {
- project: string
- path: Path
-}
-
-export async function* ls(): AsyncGenerator {
- for (const prefix of pantry_paths()) {
- for await (const path of _ls_pantry(prefix)) {
- yield {
- project: path.parent().relative({ to: prefix }),
- path
- }
- }
- }
-}
-
-async function* _ls_pantry(dir: Path): AsyncGenerator {
- if (!dir.isDirectory()) throw new TeaError('not-found: pantry', { path: dir })
-
- for await (const [path, { name, isDirectory }] of dir.ls()) {
- if (isDirectory) {
- for await (const x of _ls_pantry(path)) {
- yield x
- }
- } else if (name === "package.yml" || name === "package.yaml") {
- yield path
- }
- }
-}
diff --git a/src/hooks/usePrefix.ts b/src/hooks/usePrefix.ts
deleted file mode 100644
index 93479d316..000000000
--- a/src/hooks/usePrefix.ts
+++ /dev/null
@@ -1,16 +0,0 @@
-import Path from "path"
-import useConfig from "hooks/useConfig.ts"
-
-export class Prefix extends Path {
- www: Path
-
- constructor(prefix: Path) {
- super(prefix)
- this.www = prefix.join("tea.xyz/var/www")
- }
-}
-
-export default function usePrefix() {
- const { teaPrefix } = useConfig()
- return new Prefix(teaPrefix)
-}
diff --git a/src/hooks/useRun.ts b/src/hooks/useRun.ts
index 890ed1883..0a724605f 100644
--- a/src/hooks/useRun.ts
+++ b/src/hooks/useRun.ts
@@ -1,5 +1,5 @@
-import Path from "path"
-import { isArray } from "is_what"
+import { isArray } from "is-what"
+import { Path } from "tea"
export interface RunOptions extends Omit {
cmd: (string | Path)[] | Path
@@ -19,7 +19,7 @@ export class RunError extends Error {
export default async function useRun({ spin, ...opts }: RunOptions) {
const cmd = isArray(opts.cmd) ? opts.cmd.map(x => `${x}`) : [opts.cmd.string]
const cwd = opts.cwd?.toString()
- console.verbose({ cwd, ...opts, cmd })
+ console.log({ cwd, ...opts, cmd })
const stdio = { stdout: 'inherit', stderr: 'inherit', stdin: 'inherit' } as Pick
if (spin) {
@@ -30,7 +30,7 @@ export default async function useRun({ spin, ...opts }: RunOptions) {
try {
proc = _internals.nativeRun(cmd.shift()!, { ...opts, args: cmd, cwd, ...stdio }).spawn()
const exit = await proc.status
- console.verbose({ exit })
+ console.log({ exit })
if (!exit.success) throw new RunError(exit.code, cmd)
} catch (err) {
if (spin && proc) {
diff --git a/src/hooks/useShellEnv.ts b/src/hooks/useShellEnv.ts
deleted file mode 100644
index b166cbc7b..000000000
--- a/src/hooks/useShellEnv.ts
+++ /dev/null
@@ -1,159 +0,0 @@
-import { Installation } from "types"
-import { OrderedHashSet } from "rimbu/ordered/set/index.ts"
-import { host } from "utils"
-import { usePrefix, usePantry } from "hooks"
-
-export const EnvKeys = [
- 'PATH',
- 'MANPATH',
- 'PKG_CONFIG_PATH',
- 'LIBRARY_PATH',
- 'LD_LIBRARY_PATH',
- 'CPATH',
- 'XDG_DATA_DIRS',
- 'CMAKE_PREFIX_PATH',
- 'DYLD_FALLBACK_LIBRARY_PATH',
- 'SSL_CERT_FILE',
- 'LDFLAGS',
- 'TEA_PREFIX',
- 'ACLOCAL_PATH'
-] as const
-export type EnvKey = typeof EnvKeys[number]
-
-interface Options {
- installations: Installation[]
-}
-
-/// returns an environment that supports the provided packages
-export default async function useShellEnv({installations}: Options): Promise> {
- const {getRuntimeEnvironment} = usePantry()
-
- const vars: Partial>> = {}
- const isMac = host().platform == 'darwin'
-
- const projects = new Set(installations.map(x => x.pkg.project))
- const has_cmake = projects.has('cmake.org')
- const archaic = true
-
- const rv: Record = {}
- const seen = new Set()
-
- for (const installation of installations) {
-
- if (!seen.insert(installation.pkg.project).inserted) {
- console.warn("warning: env is being duped:", installation.pkg.project)
- }
-
- for (const key of EnvKeys) {
- for (const suffix of suffixes(key)!) {
- vars[key] = compact_add(vars[key], installation.path.join(suffix).compact()?.string)
- }
- }
-
- if (archaic) {
- vars.LIBRARY_PATH = compact_add(vars.LIBRARY_PATH, installation.path.join("lib").compact()?.string)
- vars.CPATH = compact_add(vars.CPATH, installation.path.join("include").compact()?.string)
- }
-
- if (has_cmake) {
- vars.CMAKE_PREFIX_PATH = compact_add(vars.CMAKE_PREFIX_PATH, installation.path.string)
- }
-
- if (projects.has('gnu.org/autoconf')) {
- vars.ACLOCAL_PATH = compact_add(vars.ACLOCAL_PATH, installation.path.join("share/aclocal").compact()?.string)
- }
-
- if (installation.pkg.project === 'openssl.org') {
- const certPath = installation.path.join("ssl/cert.pem").compact()?.string
- // this is a single file, so we assume a
- // valid entry is correct
- if (certPath) vars.SSL_CERT_FILE = OrderedHashSet.of(certPath)
- }
-
- // pantry configured runtime environment
- const runtime = await getRuntimeEnvironment(installation.pkg)
- for (const key in runtime) {
- rv[key] ??= []
- rv[key].push(runtime[key])
- }
- }
-
- // this is how we use precise versions of libraries
- // for your virtual environment
- //FIXME SIP on macOS prevents DYLD_FALLBACK_LIBRARY_PATH from propagating to grandchild processes
- if (vars.LIBRARY_PATH) {
- vars.LD_LIBRARY_PATH = vars.LIBRARY_PATH
- if (isMac) {
- // non FALLBACK variety causes strange issues in edge cases
- // where our symbols somehow override symbols from the macOS system
- vars.DYLD_FALLBACK_LIBRARY_PATH = vars.LIBRARY_PATH
- }
- }
-
- for (const key of EnvKeys) {
- //FIXME where is this `undefined` __happening__?
- if (vars[key] === undefined || vars[key]!.isEmpty) continue
- rv[key] = vars[key]!.toArray()
- }
-
- if (isMac) {
- // required to link to our libs
- // tea.xyz/gx/cc automatically adds this, but use of any other compilers will not
- rv["LDFLAGS"] = [`-Wl,-rpath,${usePrefix()}`]
- }
-
- // don’t break `man` lol
- rv["MANPATH"]?.push("/usr/share/man")
-
- return rv
-}
-
-function suffixes(key: EnvKey) {
- switch (key) {
- case 'PATH':
- return ["bin", "sbin"]
- case 'MANPATH':
- return ["share/man"]
- case 'PKG_CONFIG_PATH':
- return ['share/pkgconfig', 'lib/pkgconfig']
- case 'XDG_DATA_DIRS':
- return ['share']
- case 'LIBRARY_PATH':
- case 'LD_LIBRARY_PATH':
- case 'DYLD_FALLBACK_LIBRARY_PATH':
- case 'CPATH':
- case 'CMAKE_PREFIX_PATH':
- case 'SSL_CERT_FILE':
- case 'LDFLAGS':
- case 'TEA_PREFIX':
- case 'ACLOCAL_PATH':
- return [] // we handle these specially
- default: {
- const exhaustiveness_check: never = key
- throw new Error(`unhandled id: ${exhaustiveness_check}`)
- }}
-}
-
-export function expand(env: Record) {
- let rv = ''
- for (const [key, value] of Object.entries(env)) {
- if (value.length == 0) continue
- rv += `export ${key}="${value.join(":")}"\n`
- }
- return rv
-}
-
-export function flatten(env: Record) {
- const rv: Record = {}
- for (const [key, value] of Object.entries(env)) {
- rv[key] = value.join(":")
- }
- return rv
-}
-
-function compact_add(set: OrderedHashSet | undefined, item: T | null | undefined): OrderedHashSet {
- if (!set) set = OrderedHashSet.empty()
- if (item) set = set.add(item)
-
- return set
-}
diff --git a/src/hooks/useSync.ts b/src/hooks/useSync.ts
deleted file mode 100644
index 0f9f272fb..000000000
--- a/src/hooks/useSync.ts
+++ /dev/null
@@ -1,77 +0,0 @@
-import { useCellar, useConfig, useDownload, usePantry } from "hooks"
-import useRun, { RunOptions } from "./useRun.ts"
-import useLogger from "./useLogger.ts"
-import * as semver from "semver"
-import Path from "path"
-import { host } from "../utils/index.ts";
-
-export default async function() {
- const logger = useLogger()
- const pantry_dir = usePantry().prefix.parent()
-
- logger.replace("syncing pantries…")
-
- const { rid } = await Deno.open(pantry_dir.mkpath().string)
- await Deno.flock(rid, true)
-
- try {
- //TODO if there was already a lock, just wait on it, don’t do the following stuff
-
- const git_dir = pantry_dir.parent().join("pantries/teaxyz/pantry")
-
- if (git_dir.join("HEAD").isFile()) {
- await git("-C", git_dir, "fetch", "origin", "--force", "main:main")
- } else {
- await git("clone", "--bare", "--depth=1", "https://github.com/teaxyz/pantry", git_dir)
- }
-
- await git("--git-dir", git_dir, "--work-tree", pantry_dir, "checkout", "--force")
-
- } catch {
- // git failure or no git installed
- // ∴ download the latest tarball and uncompress over the top
- //FIXME deleted packages will not be removed with this method
- //TODO parallelize
- const src = new URL(`https://github.com/teaxyz/pantry/archive/refs/heads/main.tar.gz`)
- const tgz = await useDownload().download({ src })
- await run({cmd: ["tar", "xzf", tgz, "--strip-components=1"], cwd: pantry_dir })
- } finally {
- //TODO if this gets stuck then nothing will work so need a handler for that
- await Deno.funlock(rid)
- Deno.close(rid) // docs aren't clear if we need to do this or not
- }
-
- logger.replace("pantries sync’d ⎷")
-}
-
-//////////////////////// utils
-
-/// we support a tea installed or system installed git, nothing else
-/// eg. `git` could be a symlink in `PATH` to tea, which would cause a fork bomb
-/// on darwin if xcode or xcode/clt is not installed this will fail to our http fallback above
-async function git(...args: (string | Path)[]) {
- const pkg = await useCellar().has({ project: 'git-scm.org', constraint: new semver.Range('*') })
- const git = (pkg?.path ?? usr())?.join("bin/git")
- if (!git) throw new Error("no-git") // caught above to trigger http download instead
- await run({cmd: [git, ...args]})
-
- function usr() {
- // only return /usr/bin if in the PATH so user can explicitly override this
- const rv = Deno.env.get("PATH")?.split(":")?.includes("/usr/bin") ? new Path("/usr") : undefined
-
- /// don’t cause macOS to abort and then prompt the user to install the XcodeCLT
- //FIXME test! but this is hard to test without docker images or something!
- if (host().platform == 'darwin') {
- if (new Path("/Library/Developer/CommandLineTools/usr/bin/git").isExecutableFile()) return rv
- if (new Path("/Application/Xcode.app").isDirectory()) return rv
- return // don’t use `git`
- }
-
- return rv
- }
-}
-
-function run(opts: RunOptions) {
- const spin = useConfig().verbosity < 1
- return useRun({ ...opts, spin })
-}
diff --git a/src/hooks/useVersion.ts b/src/hooks/useVersion.ts
index 1248113b2..8ede4baa7 100644
--- a/src/hooks/useVersion.ts
+++ b/src/hooks/useVersion.ts
@@ -1,8 +1,9 @@
import { README } from "./useVirtualEnv.ts"
+import { Path } from "tea"
const version = `${(
await README(
- new URL(import.meta.url).path()
+ new Path(new URL(import.meta.url).pathname)
.parent().parent().parent()
.join("README.md")
).swallow(/not-found/))?.version}+dev`
diff --git a/src/hooks/useVirtualEnv.ts b/src/hooks/useVirtualEnv.ts
index 07a09a952..5cc777425 100644
--- a/src/hooks/useVirtualEnv.ts
+++ b/src/hooks/useVirtualEnv.ts
@@ -1,11 +1,10 @@
import { usePackageYAMLFrontMatter, refineFrontMatter, FrontMatter } from "./usePackageYAML.ts"
-import { flatmap, pkg, TeaError, validate_plain_obj } from "utils"
-import { useEnv, useMoustaches, usePrefix } from "hooks"
-import { PackageRequirement } from "types"
-import SemVer, * as semver from "semver"
-import { isPlainObject } from "is_what"
+import { PackageRequirement, Path, SemVer, utils, TeaError, hooks, semver } from "tea"
+const { flatmap, pkg, validate } = utils
+import { isPlainObject } from "is-what"
+import useConfig from "./useConfig.ts"
+const { useMoustaches } = hooks
import { JSONC } from "jsonc"
-import Path from "path"
export interface VirtualEnv {
pkgs: PackageRequirement[]
@@ -18,11 +17,9 @@ export interface VirtualEnv {
// we call into useVirtualEnv a bunch of times
const cache: Record = {}
-export default async function(cwd: Path = Path.cwd()): Promise {
- const { TEA_DIR, getEnvAsObject } = useEnv()
-
- const teaDir = flatmap(TEA_DIR, Path.cwd().join)
- if (teaDir) cwd = teaDir
+export default async function(cwd?: Path): Promise {
+ const { TEA_DIR } = useConfig().env
+ cwd = TEA_DIR ?? cwd ?? Path.cwd()
if (cache[cwd.string]) return cache[cwd.string]
@@ -53,19 +50,19 @@ export default async function(cwd: Path = Path.cwd()): Promise {
err.cause = f
throw err
}
- if (teaDir && dir.eq(teaDir)) break
+ if (TEA_DIR && dir.eq(TEA_DIR)) break
dir = dir.parent()
}
}
const lastd = teafiles.slice(-1)[0]?.parent()
- if (teaDir) {
- srcroot = teaDir
+ if (TEA_DIR) {
+ srcroot = TEA_DIR
} else if (!srcroot || lastd?.components().length < srcroot.components().length) {
srcroot = lastd
}
- if (!srcroot) throw new TeaError("not-found: dev-env", {cwd, teaDir})
+ if (!srcroot) throw new TeaError("not-found: dev-env", {cwd, TEA_DIR})
for (const [key, value] of Object.entries(env)) {
if (key != 'TEA_PREFIX') {
@@ -80,7 +77,7 @@ export default async function(cwd: Path = Path.cwd()): Promise {
const moustaches = useMoustaches()
const foo = [
...moustaches.tokenize.host(),
- { from: "tea.prefix", to: usePrefix().string },
+ { from: "tea.prefix", to: useConfig().prefix.string },
{ from: "home", to: Path.home().string },
{ from: "srcroot", to: srcroot!.string}
]
@@ -185,7 +182,7 @@ export default async function(cwd: Path = Path.cwd()): Promise {
flatmap(semver.parse(json?.version), v => version = v)
}
if (_if("action.yml", "action.yaml")) {
- const yaml = validate_plain_obj(await f!.readYAML())
+ const yaml = validate.obj(await f!.readYAML())
const [,v] = yaml.runs?.using.match(/node(\d+)/) ?? []
pkgs.push({
project: "nodejs.org",
@@ -237,21 +234,6 @@ export default async function(cwd: Path = Path.cwd()): Promise {
if (_if_d(".hg", ".svn")) {
srcroot ??= f
}
- if ((f = dir.join(".envrc").isFile())) {
- //TODO really we should pkg `direnv` install it if we find this file and configure it to do the following
- const subst = getEnvAsObject()
- subst.SRCROOT = "{{srcroot}}"
- subst.TEA_PREFIX = "{{tea.prefix}}"
- subst.VERSION = "{{version}}"
- for await (const line of f!.readLines()) {
- let [,key,value] = line.match(/^export (\S+)=(.*)$/) ?? []
- if (key && value) for (const [key, value_subst] of Object.entries(subst)) {
- value = value.replaceAll(`$${key}`, value_subst)
- }
- env[key] = value
- }
- srcroot ??= f
- }
}
}
diff --git a/src/init.ts b/src/init.ts
deleted file mode 100644
index 56090b2e6..000000000
--- a/src/init.ts
+++ /dev/null
@@ -1,103 +0,0 @@
-import { Flags } from "./args.ts"
-import { applyConfig, Config, Env } from "hooks/useConfig.ts"
-import Path from "path";
-import { isNumber } from "is_what"
-import { Verbosity } from "./types.ts";
-import { flatmap } from "utils"
-
-export function init(flags?: Flags) {
- const config = createConfig(flags)
- applyConfig(config)
-}
-
-export function createConfig(flags?: Flags): Config {
- const env = collectEnv()
-
- const isCI = !!env.CI
-
- const execPath = new Path(Deno.execPath())
- const loggerGlobalPrefix = (!Deno.isatty(Deno.stdout.rid) || isCI) ? "tea:" : undefined
- const teaPrefix = findTeaPrefix(execPath, env.TEA_PREFIX)
-
- const verbosity = getVerbosity(flags?.verbosity, env)
- const dryrun = !!flags?.dryrun
- const keepGoing = !!flags?.keepGoing
-
- const json = !!flags?.json
-
- return {
- isCI,
- execPath,
- loggerGlobalPrefix,
- teaPrefix,
- verbosity,
- dryrun,
- keepGoing,
- verbose: verbosity >= Verbosity.loud,
- debug: verbosity >= Verbosity.debug,
- silent: verbosity <= Verbosity.quiet,
- env,
- json,
- }
-}
-
-export function collectEnv(): Env {
- return {
- CI: Deno.env.get("CI"),
- CLICOLOR: Deno.env.get("CLICOLOR"),
- CLICOLOR_FORCE: Deno.env.get("CLICOLOR_FORCE"),
- DEBUG: Deno.env.get("DEBUG"),
- GITHUB_ACTIONS: Deno.env.get("GITHUB_ACTIONS"),
- GITHUB_TOKEN: Deno.env.get("GITHUB_TOKEN"),
- NO_COLOR: Deno.env.get("NO_COLOR"),
- PATH: Deno.env.get("PATH"),
- RUNNER_DEBUG: Deno.env.get("RUNNER_DEBUG"),
- SHELL: Deno.env.get("SHELL"),
- SRCROOT: Deno.env.get("SRCROOT"),
- TEA_DIR: Deno.env.get("TEA_DIR"),
- TEA_FILES: Deno.env.get("TEA_FILES"),
- TEA_FORK_BOMB_PROTECTOR: Deno.env.get("TEA_FORK_BOMB_PROTECTOR"),
- TEA_MAGIC: Deno.env.get("TEA_MAGIC"),
- TEA_PANTRY_PATH: Deno.env.get("TEA_PANTRY_PATH"),
- TEA_PKGS: Deno.env.get("TEA_PKGS"),
- TEA_PREFIX: Deno.env.get("TEA_PREFIX"),
- TEA_REWIND: Deno.env.get("TEA_REWIND"),
- VERBOSE: Deno.env.get("VERBOSE"),
- VERSION: Deno.env.get("VERSION")
- }
-}
-
-export const findTeaPrefix = (execPath: Path, envVar?: string) => {
- //NOTE doesn't work for scripts as Deno.run doesn't push through most env :/
- if (envVar) {
- return new Path(envVar)
- } else {
- // we’re either deno.land/vx/bin/deno, tea.xyz/vx/bin/tea or some symlink to the latter
- const shelf = execPath
- .readlink() // resolves the leaf symlink (if any)
- .parent()
- .parent()
- .parent()
-
- switch (shelf.basename()) {
- case 'tea.xyz':
- case 'deno.land':
- return shelf.parent()
- default:
- // we’re being generous for users who just download `tea` by itself
- // and execute it without installing it in a sanctioned structure
- return Path.home().join(".tea")
- }
- }
-}
-
-function getVerbosity(v: number | undefined, env: Env): Verbosity {
- const { DEBUG, GITHUB_ACTIONS, RUNNER_DEBUG, VERBOSE } = env
-
- if (isNumber(v)) return v
- if (DEBUG == '1') return Verbosity.debug
- if (GITHUB_ACTIONS == 'true' && RUNNER_DEBUG == '1') return Verbosity.debug
-
- const verbosity = flatmap(VERBOSE, parseInt)
- return isNumber(verbosity) ? verbosity : Verbosity.normal
-}
diff --git a/src/prefab/README.md b/src/prefab/README.md
deleted file mode 100644
index 48703d16e..000000000
--- a/src/prefab/README.md
+++ /dev/null
@@ -1,7 +0,0 @@
-# Prefabs
-
-* **Don’t** offer informative messages
- * Thus they should be units that represent something you would inform the
- user about *before* you call them
-* **Do** copious verbose messages
-* **Don’t** do stdout, they return data you *may* want to stdout
diff --git a/src/prefab/hydrate.ts b/src/prefab/hydrate.ts
deleted file mode 100644
index 8af0c295a..000000000
--- a/src/prefab/hydrate.ts
+++ /dev/null
@@ -1,154 +0,0 @@
-import { PackageRequirement, Package } from "types"
-import { isArray } from "is_what"
-import * as semver from "semver"
-import { usePantry } from "hooks"
-import "utils"
-
-
-//TODO linktime cyclic dependencies cannot be allowed
-//NOTE however if they aren’t link time it's presumably ok in some scenarios
-// eg a tool that lists a directory may depend on a tool that identifies the
-// mime types of files which could depend on the listing tool
-//FIXME actually we are not refining the constraints currently
-//TODO we are not actually restricting subsequent asks, eg. deno^1 but then deno^1.2
-
-
-interface ReturnValue {
- /// full list topologically sorted (ie dry + wet)
- pkgs: PackageRequirement[]
-
- /// your input, but version constraints refined based on the whole graph
- /// eg. you hydrate the graph for a and b, but b depends on a tighter range of a than you input
- dry: PackageRequirement[]
-
- /// packages that were not supplied to input or that require bootstrap
- wet: PackageRequirement[]
-
- /// the graph cycles at these packages
- /// this is only a problem if you need to build one of these,
- // in which case TADA! here's the list!
- bootstrap_required: Set
-}
-
-const get = (x: PackageRequirement) => usePantry().getDeps(x)
-
-/// sorts a list of packages topologically based on their
-/// dependencies. Throws if there is a cycle in the input.
-/// ignores changes in dependencies based on versions
-export default async function hydrate(
- input: (PackageRequirement | Package)[] | (PackageRequirement | Package),
- get_deps: (pkg: PackageRequirement, dry: boolean) => Promise = get,
-): Promise
-{
- if (!isArray(input)) input = [input]
-
- const dry = condense(input.map(spec => {
- if ("version" in spec) {
- return {project: spec.project, constraint: new semver.Range(`=${spec.version}`)}
- } else {
- return spec
- }
- }))
-
- const graph: Record = {}
- const bootstrap = new Set()
- const initial_set = new Set(dry.map(x => x.project))
-
- const go = async (target: Node) => {
- /// we trace up a target pkg’s dependency graph
- /// the target pkg is thus the youngest child and we are ascending up its parents
- const ascend = async (node: Node, children: Set) => {
-
- for (const dep of await get_deps(node.pkg, initial_set.has(node.project))) {
-
- if (children.has(dep.project)) {
- if (!bootstrap.has(dep.project)) {
- console.warn(`${dep.project} must be bootstrapped to build ${node.project}`)
-
- //TODO the bootstrap should keep the version constraint since it may be different
- bootstrap.add(dep.project)
- }
- } else {
- const found = graph[dep.project]
- if (found) {
- /// we already traced this graph
-
- if (found.count() < node.count()) {
- found.parent = node
- }
-
- //FIXME strictly we only have to constrain graphs that contain linkage
- // ie. you cannot have a binary that links two separate versions of eg. openssl
- // or (maybe) services, eg. you might suffer if there are two versions of postgres running (though tea mitigates this)
- found.pkg.constraint = semver.intersect(found.pkg.constraint, dep.constraint)
-
- } else {
- const new_node = new Node(dep, node)
- graph[dep.project] = new_node
- await ascend(new_node, new Set([...children, dep.project]))
- }
- }
- }
- }
- await ascend(target, new Set([target.project]))
- }
-
- for (const pkg of dry) {
- if (pkg.project in graph) {
- graph[pkg.project].pkg.constraint = semver.intersect(graph[pkg.project].pkg.constraint, pkg.constraint)
- } else {
- const new_node = new Node(pkg)
- graph[pkg.project] = new_node
- await go(new_node)
- }
- }
-
- const pkgs = Object.values(graph)
- .sort((a, b) => b.count() - a.count())
- .map(({pkg}) => pkg)
-
- //TODO strictly we need to record precisely the bootstrap version constraint
- const bootstrap_required = new Set(pkgs.compact(({project}) => bootstrap.has(project) && project))
-
- return {
- pkgs,
- dry: pkgs.filter(({project}) => initial_set.has(project)),
- wet: pkgs.filter(({project}) => !initial_set.has(project) || bootstrap_required.has(project)),
- bootstrap_required
- }
-}
-
-function condense(pkgs: PackageRequirement[]) {
- const out: PackageRequirement[] = []
- for (const pkg of pkgs) {
- const found = out.find(x => x.project === pkg.project)
- if (found) {
- found.constraint = semver.intersect(found.constraint, pkg.constraint)
- } else {
- out.push(pkg)
- }
- }
- return out
-}
-
-
-/////////////////////////////////////////////////////////////////////////// lib
-class Node {
- parent: Node | undefined
- readonly pkg: PackageRequirement
- readonly project: string
-
- constructor(pkg: PackageRequirement, parent?: Node) {
- this.parent = parent
- this.pkg = pkg
- this.project = pkg.project
- }
-
- count(): number {
- let n = 0
- let node = this as Node | undefined
- // deno-lint-ignore no-cond-assign
- while (node = node?.parent) n++
- return n
- }
-}
diff --git a/src/prefab/index.ts b/src/prefab/index.ts
deleted file mode 100644
index f6adb339d..000000000
--- a/src/prefab/index.ts
+++ /dev/null
@@ -1,11 +0,0 @@
-import hydrate from "./hydrate.ts"
-import install from "./install.ts"
-import link from "./link.ts"
-import resolve from "./resolve.ts"
-
-export {
- hydrate,
- install,
- link,
- resolve
-}
diff --git a/src/prefab/install.ts b/src/prefab/install.ts
deleted file mode 100644
index 945979a8f..000000000
--- a/src/prefab/install.ts
+++ /dev/null
@@ -1,163 +0,0 @@
-import { usePrefix, useCache, useCellar, useDownload, useOffLicense, useFetch, useConfig } from "hooks"
-import { host, panic, pkg as pkgutils, undent } from "utils"
-import useLogger, { Logger, red, teal, gray, logJSON } from "hooks/useLogger.ts"
-import { ExitError, Installation, StowageNativeBottle } from "types"
-import { crypto, toHashString } from "deno/crypto/mod.ts"
-import { Package } from "types"
-import Path from "path"
-
-export default async function install(pkg: Package, logger?: Logger): Promise {
- const { project, version } = pkg
- logger ??= useLogger(pkgutils.str(pkg))
-
- const cellar = useCellar()
- const tea_prefix = usePrefix()
- const { isCI, dryrun, json, env } = useConfig()
- const compression = get_compression(isCI)
- const stowage = StowageNativeBottle({ pkg: { project, version }, compression })
- const url = useOffLicense('s3').url(stowage)
- const tarball = useCache().path(stowage)
- const shelf = tea_prefix.join(pkg.project)
-
- const pkg_prefix_str = (pkg: Package) => [
- gray(usePrefix().prettyString()),
- pkg.project,
- `${gray('v')}${pkg.version}`
- ].join(gray('/'))
-
- if (env.TEA_MAGIC?.split(":").includes("prompt")) {
- if (!Deno.isatty(Deno.stdin.rid)) {
- throw new Error("TEA_MAGIC=prompt but stdin is not a tty")
- }
-
- do {
- const val = prompt(undent`
- ┌ ⚠️ tea requests to install ${pkg_prefix_str(pkg)}
- └ \x1B[1mallow?\x1B[0m [y/n]`
- )?.toLowerCase()
-
- if (val === "y") {
- break
- }
- if (val === "n") {
- throw new ExitError(1)
- }
- } while (true)
- }
-
- const log_install_msg = (install: Installation, title = 'installed') => {
- if (json) {
- logJSON({status: title, pkg: pkgutils.str(install.pkg)})
- } else {
- const str = pkg_prefix_str(install.pkg)
- logger!.replace(`${title}: ${str}`, { prefix: false })
- }
- }
-
- if (dryrun) {
- const install = { pkg, path: tea_prefix.join(pkg.project, `v${pkg.version}`) }
- log_install_msg(install, 'imagined')
- return install
- }
-
- if (!json) {
- logger.replace(teal("locking"))
- } else {
- logJSON({status: "locking", pkg: pkgutils.str(pkg) })
- }
- const { rid } = await Deno.open(shelf.mkpath().string)
- await Deno.flock(rid, true)
-
- try {
- const already_installed = await cellar.has(pkg)
- if (already_installed) {
- // some other tea instance installed us while we were waiting for the lock
- // or potentially we were already installed and the caller is naughty
- if (!json) {
- logger.replace(teal("installed"))
- } else {
- logJSON({status: "installed", pkg: pkgutils.str(pkg) })
- }
- return already_installed
- }
-
- if (!json) {
- logger.replace(teal("querying"))
- } else {
- logJSON({status: "querying", pkg: pkgutils.str(pkg) })
- }
-
- let stream = await useDownload().stream({ src: url, logger, dst: tarball })
- const is_downloading = stream !== undefined
- stream ??= await Deno.open(tarball.string, {read: true}).then(f => f.readable) ?? panic()
- const tar_args = compression == 'xz' ? 'xJ' : 'xz' // laughably confusing
- const tee = stream.tee()
- const pp: Promise[] = []
-
- if (is_downloading) { // cache the download (write stream to disk)
- tarball.parent().mkpath()
- const f = await Deno.open(tarball.string, {create: true, write: true, truncate: true})
- const teee = tee[1].tee()
- pp.push(teee[0].pipeTo(f.writable))
- tee[1] = teee[1]
- }
-
- const tmpdir = Path.mktemp({
- prefix: pkg.project.replaceAll("/", "_") + "_",
- dir: usePrefix().join("local/tmp")
- //NOTE ^^ inside tea prefix to avoid TMPDIR is on a different volume problems
- })
- const untar = new Deno.Command("tar", {
- args: [tar_args, "--strip-components", (pkg.project.split("/").length + 1).toString()],
- stdin: 'piped', stdout: "inherit", stderr: "inherit",
- cwd: tmpdir.string,
- }).spawn()
-
- pp.unshift(
- crypto.subtle.digest("SHA-256", tee[0]).then(toHashString),
- remote_SHA(new URL(`${url}.sha256sum`)),
- untar.status,
- tee[1].pipeTo(untar.stdin)
- )
-
- const [computed_hash_value, checksum, tar_exit_status] = await Promise.all(pp) as [string, string, Deno.CommandStatus]
-
- if (!tar_exit_status.success) {
- throw new Error(`tar exited with status ${tar_exit_status.code}`)
- }
-
- if (computed_hash_value != checksum) {
- if (!json) logger.replace(red('error'))
- tarball.rm()
- console.error("we deleted the invalid tarball. try again?")
- throw new Error(`sha: expected: ${checksum}, got: ${computed_hash_value}`)
- }
-
- const path = tmpdir.mv({ to: shelf.join(`v${pkg.version}`) })
- const install = { pkg, path }
-
- log_install_msg(install)
-
- return install
- } catch (err) {
- tarball.rm() //FIXME resumable downloads!
- throw err
- } finally {
- await Deno.funlock(rid)
- Deno.close(rid) // docs aren't clear if we need to do this or not
- }
-}
-
-async function remote_SHA(url: URL) {
- const rsp = await useFetch(url)
- if (!rsp.ok) throw rsp
- const txt = await rsp.text()
- return txt.split(' ')[0]
-}
-
-function get_compression(isCI: boolean) {
- if (isCI) return 'gz' // in CI CPU is more constrained than bandwidth
- if (host().platform == 'darwin') return 'xz' // most users are richer in CPU than bandwidth
- // TODO determine if `tar` can handle xz
- return 'gz'
-}
diff --git a/src/prefab/link.ts b/src/prefab/link.ts
deleted file mode 100644
index f3788e053..000000000
--- a/src/prefab/link.ts
+++ /dev/null
@@ -1,52 +0,0 @@
-import { Package, Installation } from "types"
-import { useCellar, useConfig } from "hooks"
-import SemVer, * as semver from "semver"
-import { panic } from "utils"
-import Path from "path"
-
-
-export default async function link(pkg: Package | Installation) {
- if (useConfig().dryrun) return
-
- const installation = await useCellar().resolve(pkg)
- pkg = installation.pkg
-
- const versions = (await useCellar()
- .ls(installation.pkg.project))
- .map(({pkg: {version}, path}) => [version, path] as [SemVer, Path])
- .sort(([a],[b]) => a.compare(b))
-
- if (versions.length <= 0) {
- console.error(pkg, installation)
- throw new Error(`no versions`)
- }
-
- const shelf = installation.path.parent()
- const newest = versions.slice(-1)[0]
- const vMm = `${pkg.version.major}.${pkg.version.minor}`
- const minorRange = new semver.Range(`^${vMm}`)
- const mostMinor = versions.filter(v => minorRange.satisfies(v[0])).at(-1) ?? panic()
-
- if (mostMinor[0].neq(pkg.version)) return
- // ^^ if we’re not the most minor we definitely not the most major
-
- await makeSymlink(`v${vMm}`)
-
- const majorRange = new semver.Range(`^${pkg.version.major.toString()}`)
- const mostMajor = versions.filter(v => majorRange.satisfies(v[0])).at(-1) ?? panic()
-
- if (mostMajor[0].neq(pkg.version)) return
- // ^^ if we’re not the most major we definitely aren’t the newest
-
- await makeSymlink(`v${pkg.version.major}`)
-
- if (pkg.version.eq(newest[0])) {
- await makeSymlink('v*')
- }
-
- async function makeSymlink(symname: string) {
- const to = shelf.join(symname)
- console.verbose({ "symlinking:": to })
- await shelf.symlink({ from: (await installation).path, to, force: true })
- }
-}
diff --git a/src/prefab/resolve.ts b/src/prefab/resolve.ts
deleted file mode 100644
index ddc5c7ff5..000000000
--- a/src/prefab/resolve.ts
+++ /dev/null
@@ -1,50 +0,0 @@
-import { Package, PackageRequirement, Installation } from "types"
-import { useCellar, useInventory } from "hooks"
-import { TeaError } from "utils"
-
-/// NOTE resolves to bottles
-/// NOTE contract there are no duplicate projects in input
-
-interface RT {
- /// fully resolved list (includes both installed and pending)
- pkgs: Package[]
-
- /// already installed packages
- installed: Installation[]
-
- /// these are the pkgs that aren’t yet installed
- pending: Package[]
-}
-
-/// resolves a list of package specifications based on what is available in
-/// bottle storage if `update` is false we will return already installed pkgs
-/// that resolve so if we are resolving `node>=12`, node 13 is installed, but
-/// node 19 is the latest we return node 13. if `update` is true we return node
-/// 19 and *you will need to install it*.
-export default async function resolve(reqs: (Package | PackageRequirement)[], {update}: {update: boolean} = {update: false}): Promise
-# tea/cli 0.32.1
+# tea/cli 0.33.0
`tea` puts the whole open source ecosystem at your fingertips:
From 53e198e5b14b5cbe595e1f4672e1f0bc83eabf6e Mon Sep 17 00:00:00 2001
From: Max Howell
Date: Tue, 23 May 2023 13:59:18 -0400
Subject: [PATCH 22/23] wip
---
src/hooks/useConfig.ts | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/hooks/useConfig.ts b/src/hooks/useConfig.ts
index 432f8dc84..5e7e14379 100644
--- a/src/hooks/useConfig.ts
+++ b/src/hooks/useConfig.ts
@@ -40,7 +40,7 @@ export interface Config extends ConfigBase {
export default function(input?: Config): Config {
if (!_internals.initialized()) {
- const rv = useConfig(input ?? panic("useConfig() not initialized")) as Config
+ const rv = useConfig(input ?? ConfigDefault()) as Config
return rv
} else {
if (input) console.warn("useConfig() already initialized, new parameters ignored")
From d70425a927ded7945ca550c3900f50d528a19ed5 Mon Sep 17 00:00:00 2001
From: Max Howell
Date: Tue, 23 May 2023 14:11:30 -0400
Subject: [PATCH 23/23] wip
---
src/hooks/useConfig.ts | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/hooks/useConfig.ts b/src/hooks/useConfig.ts
index 5e7e14379..41872743b 100644
--- a/src/hooks/useConfig.ts
+++ b/src/hooks/useConfig.ts
@@ -3,7 +3,7 @@ import { parseBool } from "../args.ts"
import { Flags } from "../args.ts"
import { isNumber } from "is-what"
import { utils, Path } from "tea"
-const { flatmap, panic } = utils
+const { flatmap } = utils
import useConfig, { Config as ConfigBase, ConfigDefault as ConfigBaseDefault, _internals } from "tea/hooks/useConfig.ts"