acrylblends 0.0.0-stage → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/acrylblends.js +4 -0
- package/dist/index.d.ts +117 -0
- package/dist/index.js +434 -0
- package/dist/index.js.map +1 -0
- package/package.json +50 -4
- package/plugins/agent/agent.ts +152 -0
- package/plugins/agent/index.ts +20 -0
- package/plugins/agent/memory.ts +24 -0
- package/plugins/agent/model.ts +79 -0
- package/plugins/agent/plugin.json +10 -0
- package/plugins/agent/tools.ts +50 -0
- package/plugins/builder/index.ts +193 -0
- package/plugins/builder/plugin.json +11 -0
- package/plugins/builder/script.ts +162 -0
- package/plugins/events/index.ts +42 -0
- package/plugins/events/plugin.json +9 -0
- package/plugins/extensions/index.ts +738 -0
- package/plugins/extensions/ledger.ts +39 -0
- package/plugins/extensions/plugin.json +12 -0
- package/plugins/extensions/resolve.ts +30 -0
- package/plugins/extensions/stage.ts +44 -0
- package/plugins/http/index.ts +64 -0
- package/plugins/http/plugin.json +10 -0
- package/plugins/instance/index.ts +167 -0
- package/plugins/instance/plugin.json +10 -0
- package/plugins/manifest/index.ts +635 -0
- package/plugins/manifest/lock.ts +94 -0
- package/plugins/manifest/plugin.json +13 -0
- package/plugins/manifest/upgrade-cli.ts +70 -0
- package/plugins/manifest/upgrade.ts +97 -0
- package/plugins/openai/index.ts +159 -0
- package/plugins/openai/plugin.json +11 -0
- package/plugins/openai/prompt.ts +31 -0
- package/plugins/sandbox/index.ts +150 -0
- package/plugins/sandbox/plugin.json +8 -0
- package/plugins/sandbox/runner.mjs +18 -0
- package/plugins/save/cli.ts +19 -0
- package/plugins/save/index.ts +55 -0
- package/plugins/save/plugin.json +12 -0
- package/plugins/server/index.ts +138 -0
- package/plugins/server/plugin.json +10 -0
- package/plugins/settings/index.ts +270 -0
- package/plugins/settings/merge.ts +79 -0
- package/plugins/settings/plugin.json +13 -0
- package/plugins/tasks/index.ts +232 -0
- package/plugins/tasks/plugin.json +11 -0
- package/plugins/tasks/store.ts +86 -0
- package/plugins/tsconfig.json +19 -0
- package/templates/agent/Dockerfile +15 -0
- package/templates/agent/README.md +52 -0
- package/templates/agent/blend.yaml +58 -0
- package/templates/agent/dockerignore +3 -0
- package/templates/agent/gitignore +2 -0
- package/templates/agent/package.json +23 -0
- package/templates/agent/src/app.ts +98 -0
- package/templates/agent/src/main.ts +25 -0
- package/templates/agent/src/routes.ts +62 -0
- package/templates/agent/template.json +16 -0
- package/templates/agent/test/agent.test.ts +45 -0
- package/templates/agent/test/app.test.ts +46 -0
- package/templates/agent/test/builder.test.ts +186 -0
- package/templates/agent/test/model.test.ts +89 -0
- package/templates/agent/tsconfig.json +19 -0
- package/templates/app/README.md +26 -0
- package/templates/app/blend.yaml +23 -0
- package/templates/app/gitignore +2 -0
- package/templates/app/package.json +23 -0
- package/templates/app/src/app.ts +61 -0
- package/templates/app/src/main.ts +20 -0
- package/templates/app/src/routes.ts +14 -0
- package/templates/app/template.json +1 -0
- package/templates/app/test/app.test.ts +56 -0
- package/templates/app/tsconfig.json +19 -0
- package/README.md +0 -3
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The ledger: one JSON line per change to the installed extensions, appended, never rewritten. Extracted from ACRYL's evolution ledger
|
|
3
|
+
* (`blend-ledger.js`): what was installed, updated, removed or refused, when, and the content digest of the version involved.
|
|
4
|
+
*/
|
|
5
|
+
import { appendFileSync, existsSync, mkdirSync, readFileSync } from 'node:fs'
|
|
6
|
+
import { dirname } from 'node:path'
|
|
7
|
+
|
|
8
|
+
export type LedgerKind =
|
|
9
|
+
| 'installed'
|
|
10
|
+
| 'updated'
|
|
11
|
+
| 'removed'
|
|
12
|
+
| 'refused'
|
|
13
|
+
| 'restored'
|
|
14
|
+
| 'requested'
|
|
15
|
+
| 'approved'
|
|
16
|
+
| 'denied'
|
|
17
|
+
|
|
18
|
+
export interface LedgerEntry {
|
|
19
|
+
readonly at: string
|
|
20
|
+
readonly kind: LedgerKind
|
|
21
|
+
readonly name: string
|
|
22
|
+
readonly version?: string
|
|
23
|
+
readonly error?: string
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function appendLedger(file: string, entry: Omit<LedgerEntry, 'at'>): LedgerEntry {
|
|
27
|
+
const line: LedgerEntry = { at: new Date().toISOString(), ...entry }
|
|
28
|
+
mkdirSync(dirname(file), { recursive: true })
|
|
29
|
+
appendFileSync(file, `${JSON.stringify(line)}\n`)
|
|
30
|
+
return line
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function readLedger(file: string): LedgerEntry[] {
|
|
34
|
+
if (!existsSync(file)) return []
|
|
35
|
+
return readFileSync(file, 'utf8')
|
|
36
|
+
.split('\n')
|
|
37
|
+
.filter((line) => line.trim() !== '')
|
|
38
|
+
.map((line) => JSON.parse(line) as LedgerEntry)
|
|
39
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "extensions",
|
|
3
|
+
"description": "Install, update, remove and reload plugins while the app runs; a failed update brings the previous version back; a ledger of every change.",
|
|
4
|
+
"requires": ["instance", "sandbox"],
|
|
5
|
+
"inject": ["appInstance"],
|
|
6
|
+
"provides": ["extensions"],
|
|
7
|
+
"dependencies": {
|
|
8
|
+
"@deepseek-ai/cordis": "4.0.4",
|
|
9
|
+
"@webboxes/blends-core": "0.4.0"
|
|
10
|
+
},
|
|
11
|
+
"mount": "mount it after instance: root.plugin(extensions, { sources: [{ dir: join(root, 'extensions'), trust: 'install' }] })"
|
|
12
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A staged extension lives in the app's home, which may be anywhere (a pinned home in a temporary folder), so a bare import such as
|
|
3
|
+
* `@deepseek-ai/cordis` would not find the app's `node_modules`. This resolve hook leaves resolution alone, and only when it fails for a
|
|
4
|
+
* module inside the staging folder, resolves the same name from the app's project folder. Uses Node's public `module.registerHooks`
|
|
5
|
+
* (ACRYL needed the same fallback on Deno); the returned function removes the hook.
|
|
6
|
+
*/
|
|
7
|
+
import { mkdirSync, realpathSync } from 'node:fs'
|
|
8
|
+
import { registerHooks } from 'node:module'
|
|
9
|
+
import { join } from 'node:path'
|
|
10
|
+
import { pathToFileURL } from 'node:url'
|
|
11
|
+
|
|
12
|
+
const isBare = (specifier: string) => !/^[./]/.test(specifier) && !specifier.includes(':')
|
|
13
|
+
|
|
14
|
+
export function resolveFromApp(stageRoot: string, appRoot: string): () => void {
|
|
15
|
+
mkdirSync(stageRoot, { recursive: true })
|
|
16
|
+
const staged = pathToFileURL(`${realpathSync(stageRoot)}/`).href
|
|
17
|
+
const parentURL = pathToFileURL(join(realpathSync(appRoot), 'package.json')).href
|
|
18
|
+
const hook = registerHooks({
|
|
19
|
+
resolve(specifier, context, nextResolve) {
|
|
20
|
+
try {
|
|
21
|
+
return nextResolve(specifier, context)
|
|
22
|
+
} catch (error) {
|
|
23
|
+
if (isBare(specifier) && context.parentURL?.startsWith(staged))
|
|
24
|
+
return nextResolve(specifier, { ...context, parentURL })
|
|
25
|
+
throw error
|
|
26
|
+
}
|
|
27
|
+
},
|
|
28
|
+
})
|
|
29
|
+
return () => hook.deregister()
|
|
30
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Staging: every installed version of an extension is a complete copy under `<home>/extensions-staged/<name>/<stamp>-<hash>/`.
|
|
3
|
+
*
|
|
4
|
+
* Extracted from ACRYL (`acryl-extension-context/lib/stage.js`). Node caches an ES module by URL, so a re-imported file keeps its old
|
|
5
|
+
* code; a copy at a new path has fresh URLs for the entry and everything it imports, so it is evaluated anew. The author's source folder
|
|
6
|
+
* is never modified. The hash makes an unchanged source recognisable; the stamp makes "newest" well defined.
|
|
7
|
+
*/
|
|
8
|
+
import { cpSync, existsSync, mkdirSync, readdirSync, rmSync } from 'node:fs'
|
|
9
|
+
import { basename, join } from 'node:path'
|
|
10
|
+
import { digestFolder, FOLDER_DIGEST_SKIPPED } from '@webboxes/blends-core'
|
|
11
|
+
|
|
12
|
+
/** A short content digest of a folder, so an unchanged source keeps its version. */
|
|
13
|
+
export const hashFolder = (dir: string, length = 12): string => digestFolder(dir, length)
|
|
14
|
+
|
|
15
|
+
/** Copy `source` into a new version folder under `stageRoot/<name>/` and return its path and version. */
|
|
16
|
+
export function stage(
|
|
17
|
+
source: string,
|
|
18
|
+
stageRoot: string,
|
|
19
|
+
name: string,
|
|
20
|
+
): { dir: string; version: string } {
|
|
21
|
+
const version = hashFolder(source)
|
|
22
|
+
const stamp = String(Date.now()).padStart(15, '0')
|
|
23
|
+
const dir = join(stageRoot, name, `${stamp}-${version}`)
|
|
24
|
+
mkdirSync(dir, { recursive: true })
|
|
25
|
+
cpSync(source, dir, {
|
|
26
|
+
recursive: true,
|
|
27
|
+
filter: (path) => !FOLDER_DIGEST_SKIPPED.has(basename(path)),
|
|
28
|
+
})
|
|
29
|
+
return { dir, version }
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Delete staged versions of `name` except the ones to keep (best effort). Modules already loaded stay in memory until the app restarts. */
|
|
33
|
+
export function prune(stageRoot: string, name: string, keep: readonly string[]): void {
|
|
34
|
+
const folder = join(stageRoot, name)
|
|
35
|
+
if (!existsSync(folder)) return
|
|
36
|
+
if (keep.length === 0) {
|
|
37
|
+
rmSync(folder, { recursive: true, force: true })
|
|
38
|
+
return
|
|
39
|
+
}
|
|
40
|
+
for (const entry of readdirSync(folder)) {
|
|
41
|
+
const path = join(folder, entry)
|
|
42
|
+
if (!keep.includes(path)) rmSync(path, { recursive: true, force: true })
|
|
43
|
+
}
|
|
44
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import type { Context } from '@deepseek-ai/cordis'
|
|
2
|
+
|
|
3
|
+
/** A request as routes see it: the method, the path and the parsed JSON body, if any. */
|
|
4
|
+
export interface RouteRequest {
|
|
5
|
+
readonly method: string
|
|
6
|
+
readonly path: string
|
|
7
|
+
readonly body: unknown
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/** What a route answers. The status defaults to 200; the body is sent as JSON. */
|
|
11
|
+
/** Sends one server-sent event: `topic` is the event name, `data` is sent as JSON. */
|
|
12
|
+
export type Send = (topic: string, data: unknown) => void
|
|
13
|
+
|
|
14
|
+
/** A live stream: called when a client connects, it returns what to run when the client leaves. */
|
|
15
|
+
export type Stream = (send: Send) => () => void
|
|
16
|
+
|
|
17
|
+
export interface Reply {
|
|
18
|
+
readonly status?: number
|
|
19
|
+
readonly body: unknown
|
|
20
|
+
/** Answer with a stream of server-sent events instead of a JSON body. */
|
|
21
|
+
readonly stream?: Stream
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export type Handler = (request: RouteRequest) => Reply | Promise<Reply>
|
|
25
|
+
|
|
26
|
+
/** The router service. Plugins add routes; the server asks it to answer requests. */
|
|
27
|
+
export interface Http {
|
|
28
|
+
/** Add a route. Returns the function that removes it, so it can be the disposer of a `ctx.effect`. */
|
|
29
|
+
route(method: string, path: string, handler: Handler): () => void
|
|
30
|
+
/** Answer a request from the registered routes, 404 when none matches. */
|
|
31
|
+
handle(request: RouteRequest): Promise<{ status: number; body: unknown; stream?: Stream }>
|
|
32
|
+
/** The registered routes, as `METHOD /path`. */
|
|
33
|
+
list(): string[]
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
declare module '@deepseek-ai/cordis' {
|
|
37
|
+
interface Context {
|
|
38
|
+
http: Http
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Provides the `http` router. Nothing listens here: see the `server` plugin. */
|
|
43
|
+
export function http(ctx: Context) {
|
|
44
|
+
const routes = new Map<string, Handler>()
|
|
45
|
+
ctx.provide('http', {
|
|
46
|
+
route(method, path, handler) {
|
|
47
|
+
const key = `${method.toUpperCase()} ${path}`
|
|
48
|
+
if (routes.has(key)) throw new Error(`route ${key} is already registered`)
|
|
49
|
+
routes.set(key, handler)
|
|
50
|
+
return () => void routes.delete(key)
|
|
51
|
+
},
|
|
52
|
+
async handle(request) {
|
|
53
|
+
const handler = routes.get(`${request.method.toUpperCase()} ${request.path}`)
|
|
54
|
+
if (!handler) return { status: 404, body: { error: 'not found' } }
|
|
55
|
+
const reply = await handler(request)
|
|
56
|
+
return {
|
|
57
|
+
status: reply.status ?? 200,
|
|
58
|
+
body: reply.body,
|
|
59
|
+
...(reply.stream ? { stream: reply.stream } : {}),
|
|
60
|
+
}
|
|
61
|
+
},
|
|
62
|
+
list: () => [...routes.keys()].sort(),
|
|
63
|
+
} satisfies Http)
|
|
64
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "http",
|
|
3
|
+
"description": "The router service: plugins add routes inside ctx.effect, so a route disappears with the plugin that added it.",
|
|
4
|
+
"requires": [],
|
|
5
|
+
"inject": [],
|
|
6
|
+
"provides": ["http"],
|
|
7
|
+
"dependencies": {
|
|
8
|
+
"@deepseek-ai/cordis": "4.0.4"
|
|
9
|
+
}
|
|
10
|
+
}
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* App instance: everything one running app owns on this machine, decided in one place and chosen once.
|
|
3
|
+
*
|
|
4
|
+
* Extracted from ACRYL (`runtime/acryl-harness-runtime/src/instance/`): a sealed compartment per app (Release It!'s bulkhead), built by
|
|
5
|
+
* factories that always produce a consistent family (home, port, run lock), chosen once in `main.ts`. `selectInstance` is the only code
|
|
6
|
+
* that reads the environment; every plugin reads the `appInstance` service instead.
|
|
7
|
+
*
|
|
8
|
+
* Pure apart from `selectInstance`, which reads the given environment object. Paths are built, never checked.
|
|
9
|
+
*/
|
|
10
|
+
import { createHash } from 'node:crypto'
|
|
11
|
+
import { basename, join, resolve } from 'node:path'
|
|
12
|
+
import type { Context } from '@deepseek-ai/cordis'
|
|
13
|
+
|
|
14
|
+
/** How the instance was chosen: from the project folder, or a home the caller pinned. */
|
|
15
|
+
export type AppInstanceKind = 'project' | 'pinned'
|
|
16
|
+
|
|
17
|
+
/** Where the app listens first, and whether it moves to the next free port when that one is taken. Port 0 means any free port. */
|
|
18
|
+
export interface PortPreference {
|
|
19
|
+
readonly start: number
|
|
20
|
+
readonly scan: boolean
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface AppInstance {
|
|
24
|
+
readonly kind: AppInstanceKind
|
|
25
|
+
/** Unique on this machine: seeds the port. */
|
|
26
|
+
readonly id: string
|
|
27
|
+
/** Human name: the project folder's name. */
|
|
28
|
+
readonly name: string
|
|
29
|
+
/** The project folder: its code and its `node_modules`. */
|
|
30
|
+
readonly root: string
|
|
31
|
+
/** The app's own data: settings, installed extensions, the extension ledger. Never shared with another app. */
|
|
32
|
+
readonly home: string
|
|
33
|
+
readonly port: PortPreference
|
|
34
|
+
/** The interface to listen on. Absent means loopback only; a container sets `0.0.0.0` so its published port reaches the app. */
|
|
35
|
+
readonly host?: string
|
|
36
|
+
/** Records the one live process of this instance. */
|
|
37
|
+
readonly runLockFile: string
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export const HOME_DIR_NAME = '.acrylblends'
|
|
41
|
+
export const RUN_LOCK_FILE = 'instance.json'
|
|
42
|
+
export const PORT_BASE = 3100
|
|
43
|
+
export const PORT_SPAN = 900
|
|
44
|
+
export const HOME_ENV = 'ACRYLBLENDS_HOME'
|
|
45
|
+
export const PORT_ENV = 'ACRYLBLENDS_PORT'
|
|
46
|
+
export const HOST_ENV = 'ACRYLBLENDS_HOST'
|
|
47
|
+
|
|
48
|
+
export class AppInstanceError extends Error {
|
|
49
|
+
override name = 'AppInstanceError'
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** FNV-1a over the id: stable across runs and platforms, spread over the app port range, so two apps rarely start on the same port. */
|
|
53
|
+
export function stablePort(id: string): number {
|
|
54
|
+
let hash = 0x811c9dc5
|
|
55
|
+
for (const character of id) {
|
|
56
|
+
hash ^= character.codePointAt(0) ?? 0
|
|
57
|
+
hash = Math.imul(hash, 0x01000193) >>> 0
|
|
58
|
+
}
|
|
59
|
+
return PORT_BASE + (hash % PORT_SPAN)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const digest = (text: string, length: number) =>
|
|
63
|
+
createHash('sha1').update(text).digest('hex').slice(0, length)
|
|
64
|
+
const plainName = (folder: string) =>
|
|
65
|
+
basename(folder)
|
|
66
|
+
.toLowerCase()
|
|
67
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
68
|
+
.replace(/^-+|-+$/g, '') || 'app'
|
|
69
|
+
|
|
70
|
+
function family(input: {
|
|
71
|
+
kind: AppInstanceKind
|
|
72
|
+
id: string
|
|
73
|
+
name: string
|
|
74
|
+
root: string
|
|
75
|
+
home: string
|
|
76
|
+
port: PortPreference
|
|
77
|
+
}): AppInstance {
|
|
78
|
+
return Object.freeze({
|
|
79
|
+
kind: input.kind,
|
|
80
|
+
id: input.id,
|
|
81
|
+
name: input.name,
|
|
82
|
+
root: input.root,
|
|
83
|
+
home: input.home,
|
|
84
|
+
port: Object.freeze({ ...input.port }),
|
|
85
|
+
runLockFile: join(input.home, RUN_LOCK_FILE),
|
|
86
|
+
})
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** The default: the project folder owns its data in `<project>/.acrylblends`. Two folders with one name get different ids. */
|
|
90
|
+
export function projectInstance(projectRoot: string): AppInstance {
|
|
91
|
+
const root = resolve(projectRoot)
|
|
92
|
+
const name = plainName(root)
|
|
93
|
+
const id = `${name}-${digest(root, 4)}`
|
|
94
|
+
return family({
|
|
95
|
+
kind: 'project',
|
|
96
|
+
id,
|
|
97
|
+
name,
|
|
98
|
+
root,
|
|
99
|
+
home: join(root, HOME_DIR_NAME),
|
|
100
|
+
port: { start: stablePort(id), scan: true },
|
|
101
|
+
})
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* A home the caller pinned (ACRYLBLENDS_HOME): honoured as given. Tests and second copies of an app use it to stay apart. `root` is the
|
|
106
|
+
* project folder (where its packages are installed); it defaults to the current directory.
|
|
107
|
+
*/
|
|
108
|
+
export function pinnedInstance(
|
|
109
|
+
home: string,
|
|
110
|
+
options: { name?: string; root?: string } = {},
|
|
111
|
+
): AppInstance {
|
|
112
|
+
const absolute = resolve(home)
|
|
113
|
+
const root = resolve(options.root ?? process.cwd())
|
|
114
|
+
const id = `pinned-${digest(absolute, 6)}`
|
|
115
|
+
return family({
|
|
116
|
+
kind: 'pinned',
|
|
117
|
+
id,
|
|
118
|
+
name: options.name ?? plainName(root),
|
|
119
|
+
root,
|
|
120
|
+
home: absolute,
|
|
121
|
+
port: { start: stablePort(id), scan: true },
|
|
122
|
+
})
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** The same family with an explicit start port. Port 0 asks the system for any free port. */
|
|
126
|
+
export function withPort(instance: AppInstance, start: number): AppInstance {
|
|
127
|
+
if (!Number.isInteger(start) || start < 0 || start > 65_535) {
|
|
128
|
+
throw new AppInstanceError(
|
|
129
|
+
`the port must be 0 (any free port) or 1 to 65535, got ${String(start)}`,
|
|
130
|
+
)
|
|
131
|
+
}
|
|
132
|
+
return Object.freeze({ ...instance, port: Object.freeze({ start, scan: start !== 0 }) })
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Choose the instance. The only place that reads the environment: `ACRYLBLENDS_HOME` pins the home, `ACRYLBLENDS_PORT` sets the start port, `ACRYLBLENDS_HOST` the interface;
|
|
137
|
+
* otherwise the project folder decides.
|
|
138
|
+
*/
|
|
139
|
+
export function selectInstance(options: {
|
|
140
|
+
projectRoot: string
|
|
141
|
+
env?: Record<string, string | undefined>
|
|
142
|
+
}): AppInstance {
|
|
143
|
+
const env = options.env ?? {}
|
|
144
|
+
const pinned = env[HOME_ENV]?.trim()
|
|
145
|
+
let instance = pinned
|
|
146
|
+
? pinnedInstance(pinned, { name: plainName(options.projectRoot), root: options.projectRoot })
|
|
147
|
+
: projectInstance(options.projectRoot)
|
|
148
|
+
const port = env[PORT_ENV]?.trim()
|
|
149
|
+
if (port) instance = withPort(instance, Number(port))
|
|
150
|
+
const host = env[HOST_ENV]?.trim()
|
|
151
|
+
if (host) instance = Object.freeze({ ...instance, host })
|
|
152
|
+
return instance
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
declare module '@deepseek-ai/cordis' {
|
|
156
|
+
interface Context {
|
|
157
|
+
appInstance: AppInstance
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/** Provides the chosen instance as the `appInstance` service. Mount it first, with the instance as its config. */
|
|
162
|
+
export const instance = {
|
|
163
|
+
name: 'instance',
|
|
164
|
+
apply(ctx: Context, chosen: AppInstance) {
|
|
165
|
+
ctx.provide('appInstance', chosen)
|
|
166
|
+
},
|
|
167
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "instance",
|
|
3
|
+
"description": "Where the app keeps things (home, port, run lock), chosen once from the environment or the project folder.",
|
|
4
|
+
"requires": [],
|
|
5
|
+
"inject": [],
|
|
6
|
+
"provides": ["appInstance"],
|
|
7
|
+
"dependencies": {
|
|
8
|
+
"@deepseek-ai/cordis": "4.0.4"
|
|
9
|
+
}
|
|
10
|
+
}
|