@lapxo/head 0.1.5 → 0.1.7
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/README.md +6 -4
- package/TARGET.bound +245 -121
- package/dist/src/access/contract.d.ts +0 -2
- package/dist/src/access/obtain.js +3 -3
- package/dist/src/ahead/budget.d.ts +4 -0
- package/dist/src/ahead/budget.js +8 -0
- package/dist/src/answers/answered.d.ts +4 -0
- package/dist/src/answers/answered.js +22 -0
- package/dist/src/claims/claim.d.ts +1 -0
- package/dist/src/claims/claim.js +15 -15
- package/dist/src/claims/view.d.ts +3 -0
- package/dist/src/claims/view.js +11 -0
- package/dist/src/compile/pin.d.ts +7 -0
- package/dist/src/compile/pin.js +8 -13
- package/dist/src/core/answer.d.ts +1 -0
- package/dist/src/core/answer.js +1 -0
- package/dist/src/core/device.d.ts +3 -6
- package/dist/src/core/device.js +7 -10
- package/dist/src/core/lock.d.ts +1 -0
- package/dist/src/core/lock.js +2 -1
- package/dist/src/core/read.js +1 -2
- package/dist/src/core/worlds.js +2 -3
- package/dist/src/doors/guard.d.ts +18 -0
- package/dist/src/doors/guard.js +63 -0
- package/dist/src/doors/head.d.ts +60 -0
- package/dist/src/doors/head.js +136 -0
- package/dist/src/doors/http.d.ts +3 -0
- package/dist/src/doors/http.js +34 -0
- package/dist/src/doors/lines.d.ts +3 -0
- package/dist/src/doors/lines.js +21 -0
- package/dist/src/flows/act.d.ts +1 -5
- package/dist/src/flows/act.js +1 -5
- package/dist/src/flows/call.d.ts +2 -9
- package/dist/src/flows/call.js +12 -12
- package/dist/src/flows/device.js +4 -6
- package/dist/src/flows/plan.js +7 -10
- package/dist/src/flows/read.d.ts +1 -7
- package/dist/src/flows/read.js +6 -9
- package/dist/src/index.js +20 -114
- package/dist/src/links/inspect.d.ts +1 -0
- package/dist/src/links/inspect.js +9 -8
- package/dist/src/onboard/first.js +1 -1
- package/dist/src/places/admit.js +1 -1
- package/dist/src/places/found.js +6 -10
- package/dist/src/ports/adapters/request.d.ts +1 -0
- package/dist/src/ports/adapters/request.js +2 -2
- package/dist/src/ports/bound.d.ts +15 -18
- package/dist/src/ports/bound.js +33 -30
- package/dist/src/ports/host.d.ts +35 -0
- package/dist/src/ports/host.js +108 -0
- package/dist/src/ports/keychain.d.ts +14 -2
- package/dist/src/ports/keychain.js +19 -7
- package/dist/src/ports/scratch.d.ts +4 -0
- package/dist/src/ports/scratch.js +10 -0
- package/dist/src/signer.js +8 -7
- package/dist/src/tools/card.js +3 -6
- package/dist/src/tools/look.js +5 -5
- package/dist/src/use/named.d.ts +12 -0
- package/dist/src/use/named.js +25 -0
- package/dist/src/verbs/ask.d.ts +1 -9
- package/dist/src/verbs/ask.js +4 -11
- package/dist/src/verbs/card.d.ts +2 -2
- package/dist/src/verbs/card.js +3 -3
- package/dist/src/walk/context.js +3 -6
- package/package.json +3 -3
- package/dist/src/ports/signers.d.ts +0 -6
- package/dist/src/ports/signers.js +0 -14
- package/dist/src/ports/spare.d.ts +0 -1
- package/dist/src/ports/spare.js +0 -33
- package/dist/src/ports/warm.d.ts +0 -19
- package/dist/src/ports/warm.js +0 -45
package/dist/src/tools/look.js
CHANGED
|
@@ -24,18 +24,18 @@ export const answer = (asked, context) => inPlace(asked, context, async (place)
|
|
|
24
24
|
if ('unsettled' in held)
|
|
25
25
|
return [sayIn(context.lock)('unsettled', { source, why: held.unsettled })];
|
|
26
26
|
const [v, first, shallow] = [vocabOf(context.lock), asked['at'] === '0', word('look/first').split('|')];
|
|
27
|
-
const region = `${v.offers}/${source}/${operation ?? ''
|
|
27
|
+
const [region, uses] = [`${v.offers}/${source}/`, held.lines.filter((l) => l.scope?.startsWith(`${word('use/family')}/${source}/`))], named = operation ? [operation] : uses.map((l) => l.value ?? '');
|
|
28
28
|
const shown = held.lines.filter((l) => {
|
|
29
29
|
const scope = l.scope ?? '';
|
|
30
|
-
if (scope === `${v.region}/${source}`)
|
|
30
|
+
if (scope === `${v.region}/${source}` || (!operation && uses.includes(l)))
|
|
31
31
|
return true;
|
|
32
32
|
if (scope.startsWith(`${v.keys}/${source}/`))
|
|
33
33
|
return !first;
|
|
34
|
-
if (!(
|
|
34
|
+
if (!named.some((op) => scope === `${region}${op}` || scope.startsWith(`${region}${op}/`)) || scope.endsWith(`/${v.carry}`))
|
|
35
35
|
return false;
|
|
36
36
|
return !first || (!scope.endsWith(`/${v.form}`) && shallow.includes(l.measure ?? ''));
|
|
37
|
-
});
|
|
38
|
-
return shown.length ? [...shown.map((l) => canonical(l)).sort(), ...(first ? [] : trustOf(context.lock, held.lines, source)), ...(operation ? await readSource(asked, context) : [])] : [sayIn(context.lock)('unread', { source })];
|
|
37
|
+
}), more = held.lines.filter((l) => l.measure === v.class && l.scope?.startsWith(region) && !named.includes(l.scope.slice(region.length))).length;
|
|
38
|
+
return shown.length ? [...shown.map((l) => canonical(l)).sort(), ...(first ? [] : trustOf(context.lock, held.lines, source)), ...(more ? [sayIn(context.lock)('more-offers', { source, more: String(more) })] : []), ...(operation ? await readSource(asked, context) : [])] : [sayIn(context.lock)('unread', { source })];
|
|
39
39
|
}
|
|
40
40
|
const view = `${asked['view'] ?? ''}${asked['at'] ? `${word('bound/at')}${asked['at']}` : ''}`;
|
|
41
41
|
return printedOr(context, await context.bound(place, [word('bound/read'), word('bound/view'), view]));
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { Context } from '../core/answer.ts';
|
|
2
|
+
import { type Said } from '../core/lock.ts';
|
|
3
|
+
/** The offers of a source a place holds: head's own registry of it, local, never sent to the model whole. */
|
|
4
|
+
export declare const offersOf: (lock: readonly Said[], lines: readonly Said[], source: string) => readonly string[];
|
|
5
|
+
/** Resolve the offer against the local registry. Ambiguity asks the person; read-ahead creates no use line. */
|
|
6
|
+
export declare const namedIn: (context: Context, lines: readonly Said[], source: string, asked: string, ahead: boolean) => Promise<{
|
|
7
|
+
readonly operation: string;
|
|
8
|
+
readonly use?: string;
|
|
9
|
+
readonly scope?: string;
|
|
10
|
+
} | {
|
|
11
|
+
readonly said: readonly string[];
|
|
12
|
+
}>;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { canonical } from '@lapxo/topos/wire';
|
|
2
|
+
import { valueOf } from "../core/lock.js";
|
|
3
|
+
import { vocabOf } from "../core/read.js";
|
|
4
|
+
import { sayIn } from "../core/surface.js";
|
|
5
|
+
/** The words of a name as a person or a model says it: its runs of letters and digits, lowercase. */
|
|
6
|
+
const words = (text) => text.toLowerCase().split(/[^a-z0-9]+/).filter(Boolean);
|
|
7
|
+
/** The offers of a source a place holds: head's own registry of it, local, never sent to the model whole. */
|
|
8
|
+
export const offersOf = (lock, lines, source) => ((at) => lines.filter((l) => l.measure === vocabOf(lock).class && l.scope?.startsWith(at)).map((l) => l.scope.slice(at.length)))(`${vocabOf(lock).offers}/${source}/`);
|
|
9
|
+
/** Resolve the offer against the local registry. Ambiguity asks the person; read-ahead creates no use line. */
|
|
10
|
+
export const namedIn = async (context, lines, source, asked, ahead) => {
|
|
11
|
+
const [v, word, say] = [vocabOf(context.lock), (k) => valueOf(context.lock, k), sayIn(context.lock)];
|
|
12
|
+
const [family, name] = [word('use/family'), words(asked).join('-')], uses = lines.filter((l) => l.scope?.startsWith(`${family}/${source}/`)), offers = offersOf(context.lock, lines, source);
|
|
13
|
+
const used = uses.find((l) => l.scope === `${family}/${source}/${name}`)?.value;
|
|
14
|
+
if (used !== undefined || ahead || !asked)
|
|
15
|
+
return { operation: used ?? asked };
|
|
16
|
+
const named = (op) => words([op, ...lines.filter((l) => l.scope === `${v.offers}/${source}/${op}/${v.carry}`).map((l) => l.value ?? '')].join(' '));
|
|
17
|
+
const found = offers.includes(asked) ? [asked] : offers.filter((op) => words(asked).length > 0 && words(asked).every((w) => named(op).includes(w)));
|
|
18
|
+
const operation = found.length > 1 ? (await context.person.choose(say('pick-offer', { source, asked }), [{ name: family, options: found.map((o) => ({ value: o, label: o })) }]))?.[family] : found[0];
|
|
19
|
+
if (!found.length)
|
|
20
|
+
return { operation: asked };
|
|
21
|
+
if (operation === undefined || !found.includes(operation))
|
|
22
|
+
return { said: [say('pick-offer', { source, asked })] };
|
|
23
|
+
const scope = `${family}/${source}/${name}`;
|
|
24
|
+
return uses.some((l) => l.value === operation) ? { operation } : { operation, scope, use: canonical({ at: `${word('founding/person-at')}use/${new Date(context.now()).toISOString()}`, by: 'target', form: 'alphabet', measure: 'operation', role: 'writes', scope, value: operation }) };
|
|
25
|
+
};
|
package/dist/src/verbs/ask.d.ts
CHANGED
|
@@ -1,11 +1,3 @@
|
|
|
1
1
|
import type { Answer } from '../core/answer.ts';
|
|
2
|
-
/**
|
|
3
|
-
* What a person asks, answered by the question it is. A view, or the region of a source or of one operation in it (what
|
|
4
|
-
* it offers, one read of it, why that answer stands, what was kept of it), is answered by look; drafted lines by plan;
|
|
5
|
-
* what is open by card; one question of several sources at once, by comparing what each said; anything else by places. Each form is read with no resolution unless the lock declares one for a
|
|
6
|
-
* person; asked why, at the one it declares to explain, and where it declares none, the same answer, said so. Nothing
|
|
7
|
-
* here writes one. An answer about one place ends with its card, taken off the fold the answer reads, valid from it.
|
|
8
|
-
* With no place at all, nothing is founded: it says so, and that connecting an API founds the person's place, the question
|
|
9
|
-
* carried along. A place still owing a source's bytes is answered only once they arrive.
|
|
10
|
-
*/
|
|
2
|
+
/** Route a question at the declared resolution, then attach the card of the place it read. */
|
|
11
3
|
export declare const asked: Answer;
|
package/dist/src/verbs/ask.js
CHANGED
|
@@ -6,18 +6,11 @@ import { sayIn } from "../core/surface.js";
|
|
|
6
6
|
import { paidIn } from "../onboard/owed.js";
|
|
7
7
|
import { pendingIn } from "../sources/refused.js";
|
|
8
8
|
import { compared } from "../marks/compare.js";
|
|
9
|
+
import { viewed } from "../claims/view.js";
|
|
9
10
|
import { cardAt } from "./card.js";
|
|
10
|
-
/**
|
|
11
|
-
* What a person asks, answered by the question it is. A view, or the region of a source or of one operation in it (what
|
|
12
|
-
* it offers, one read of it, why that answer stands, what was kept of it), is answered by look; drafted lines by plan;
|
|
13
|
-
* what is open by card; one question of several sources at once, by comparing what each said; anything else by places. Each form is read with no resolution unless the lock declares one for a
|
|
14
|
-
* person; asked why, at the one it declares to explain, and where it declares none, the same answer, said so. Nothing
|
|
15
|
-
* here writes one. An answer about one place ends with its card, taken off the fold the answer reads, valid from it.
|
|
16
|
-
* With no place at all, nothing is founded: it says so, and that connecting an API founds the person's place, the question
|
|
17
|
-
* carried along. A place still owing a source's bytes is answered only once they arrive.
|
|
18
|
-
*/
|
|
11
|
+
/** Route a question at the declared resolution, then attach the card of the place it read. */
|
|
19
12
|
export const asked = async (asked, context) => {
|
|
20
|
-
const why = asked['why'] === 'true', at = (form) => valueOf(context.lock, `resolution/${form}/${why ? 'why' : 'person'}`)
|
|
13
|
+
const why = asked['why'] === 'true', at = (form) => ((v) => (v && v !== valueOf(context.lock, 'resolution/own') ? v : undefined))(valueOf(context.lock, `resolution/${form}/${why ? 'why' : 'person'}`));
|
|
21
14
|
const read = async (form, tool, more = {}) => [...(why && at(form) === undefined ? [sayIn(context.lock)('why-undeclared', { form })] : []),
|
|
22
15
|
...await (await answerOf('tools', tool))({ ...asked, ...more, at: at(form) }, context)];
|
|
23
16
|
const place = await placeOf(asked, context), founded = place !== undefined && context.files.has(place, LOCK), carded = asked['card'] === 'true';
|
|
@@ -29,7 +22,7 @@ export const asked = async (asked, context) => {
|
|
|
29
22
|
const owed = asked['source'] && founded ? await paidIn(asked, context, place) : undefined;
|
|
30
23
|
if (owed !== undefined && !owed.answer)
|
|
31
24
|
return owed.said;
|
|
32
|
-
const answer = asked['sources'] && founded ? await compared(asked, context, place) : asked['lines'] ? await read('plan', 'plan') : asked['view'] ? await read('view', 'look') : asked['source'] ? await read('offers', 'look')
|
|
25
|
+
const answer = asked['sources'] && founded ? await compared(asked, context, place) : asked['lines'] ? await read('plan', 'plan') : founded && asked['view'] === valueOf(context.lock, 'claim/view') ? await viewed(asked, context) : asked['view'] ? await read('view', 'look') : asked['source'] ? await read('offers', 'look')
|
|
33
26
|
: !carded ? await read('places', 'places') : founded ? [...await read('card', 'card', { place }), ...await pendingIn(context, place)] : [];
|
|
34
27
|
const after = held === undefined || 'said' in held || place === undefined || !context.moved(place) ? held : await cardAt(context, place);
|
|
35
28
|
return [...(owed?.said ?? []), ...answer, ...(after === undefined || 'said' in after ? [] : [sayIn(context.lock)('card', { place: place ?? '', card: after.card, from: 'ask' })])];
|
package/dist/src/verbs/card.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { Context } from '../core/answer.ts';
|
|
2
2
|
/**
|
|
3
|
-
* The card an act is made on: the place's standing as bound folds it, with the reads it kept
|
|
4
|
-
* device acts with. Anything landed, a grant withdrawn or a key admitted or changed moves it; a read never does. A folder
|
|
3
|
+
* The card an act is made on: the place's standing as bound folds it, with the reads it kept and the uses they named set
|
|
4
|
+
* aside, and the keys this device acts with. Anything landed, a grant withdrawn or a key admitted or changed moves it; a read never does. A folder
|
|
5
5
|
* that holds no place yet has the card of nothing.
|
|
6
6
|
*/
|
|
7
7
|
export declare const cardAt: (context: Context, place: string | undefined) => Promise<{
|
package/dist/src/verbs/card.js
CHANGED
|
@@ -3,8 +3,8 @@ import { LOCK } from '@lapxo/topos/wire';
|
|
|
3
3
|
import { baseOf, standingOr } from "../core/answer.js";
|
|
4
4
|
import { valueOf } from "../core/lock.js";
|
|
5
5
|
/**
|
|
6
|
-
* The card an act is made on: the place's standing as bound folds it, with the reads it kept
|
|
7
|
-
* device acts with. Anything landed, a grant withdrawn or a key admitted or changed moves it; a read never does. A folder
|
|
6
|
+
* The card an act is made on: the place's standing as bound folds it, with the reads it kept and the uses they named set
|
|
7
|
+
* aside, and the keys this device acts with. Anything landed, a grant withdrawn or a key admitted or changed moves it; a read never does. A folder
|
|
8
8
|
* that holds no place yet has the card of nothing.
|
|
9
9
|
*/
|
|
10
10
|
export const cardAt = async (context, place) => {
|
|
@@ -12,5 +12,5 @@ export const cardAt = async (context, place) => {
|
|
|
12
12
|
if ('said' in held)
|
|
13
13
|
return held;
|
|
14
14
|
const read = `${valueOf(context.lock, 'receipt/at')}:`, keys = ['head', 'person'].map((role) => context.device(role) ?? '');
|
|
15
|
-
return { card: createHash('sha256').update([baseOf(held.lines.filter((l) => !l.at?.startsWith(read))), ...keys].join('\n')).digest('hex').slice(0, 16) };
|
|
15
|
+
return { card: createHash('sha256').update([baseOf(held.lines.filter((l) => !l.at?.startsWith(read) && !l.scope?.startsWith(`${valueOf(context.lock, 'use/family')}/`))), ...keys].join('\n')).digest('hex').slice(0, 16) };
|
|
16
16
|
};
|
package/dist/src/walk/context.js
CHANGED
|
@@ -2,6 +2,7 @@ import { join } from 'node:path';
|
|
|
2
2
|
import { canonical, LOCK } from '@lapxo/topos/wire';
|
|
3
3
|
import { members, valueOf } from "../core/lock.js";
|
|
4
4
|
import { linesOf } from "../core/worlds.js";
|
|
5
|
+
import { pinnedAs } from "../compile/pin.js";
|
|
5
6
|
/**
|
|
6
7
|
* A place made walkable once it is founded: the context another place authenticates it by — its wire, its keys and its
|
|
7
8
|
* origin, read off the lines it was born with — folded by bound into a standing, laid beside its lock and pinned by its
|
|
@@ -11,13 +12,9 @@ export const walkable = async (context, dir, account) => {
|
|
|
11
12
|
const [word, at] = [(k) => valueOf(context.lock, k), valueOf(context.lock, 'walk/context/file')];
|
|
12
13
|
const reads = members(word('walk/context/reads'));
|
|
13
14
|
const born = linesOf(context.files.take(join(dir, LOCK)) ?? new Uint8Array()).filter((l) => reads.some((r) => (r.endsWith('/**') ? l.scope?.startsWith(r.slice(0, -2)) : l.scope === r)) && l.value !== 'withdraw');
|
|
14
|
-
const
|
|
15
|
-
|
|
16
|
-
canonical({ at: 'policy:head/walk', by: 'target', form: 'alphabet', measure: 'reads', role: 'writes', scope: word('walk/context/region'), value: reads.join('|') })].join('\n')}\n`));
|
|
17
|
-
const pinned = await context.pinned(dir, draft);
|
|
18
|
-
context.files.drop(dir, draft);
|
|
15
|
+
const pinned = await pinnedAs(context, dir, at, [...born.map(({ sig: _s, epoch: _e, ...l }) => canonical({ ...l, by: 'target' })),
|
|
16
|
+
canonical({ at: 'policy:head/walk', by: 'target', form: 'alphabet', measure: 'reads', role: 'writes', scope: word('walk/context/region'), value: reads.join('|') })]);
|
|
19
17
|
if (pinned === undefined || 'refused' in pinned)
|
|
20
18
|
return { refused: pinned === undefined ? 'walk context folded nothing' : pinned.refused };
|
|
21
|
-
context.files.lay(dir, at, new TextEncoder().encode(pinned.bytes));
|
|
22
19
|
return context.landRoot(dir, [canonical({ at: 'policy:head/walk', by: 'target', form: 'alphabet', measure: 'digest', needs: at, role: 'writes', scope: 'walk/context', value: pinned.pin })], account);
|
|
23
20
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lapxo/head",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.7",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "An MCP server that folds: it runs bound as a process, hands a model what bound printed, and lands what it read as receipts.",
|
|
6
6
|
"license": "MIT",
|
|
@@ -26,8 +26,8 @@
|
|
|
26
26
|
"test": "node --test test/*.test.ts && node --test --test-concurrency=1 test/time/*.test.ts"
|
|
27
27
|
},
|
|
28
28
|
"dependencies": {
|
|
29
|
-
"@lapxo/bound": "0.2.
|
|
30
|
-
"@lapxo/topos": "0.2.
|
|
29
|
+
"@lapxo/bound": "0.2.7",
|
|
30
|
+
"@lapxo/topos": "0.2.4",
|
|
31
31
|
"@modelcontextprotocol/sdk": "1.32.1"
|
|
32
32
|
},
|
|
33
33
|
"devDependencies": {
|
|
@@ -1,6 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The host's registry bound reads to bind a declared signer name to a program: head's own signer, started by this node,
|
|
3
|
-
* told the keychain service, the account its secret is kept under, the key it signs as, the algorithm and the store this
|
|
4
|
-
* host keeps secrets in. It holds no secret; it lives beside nothing a repository ships.
|
|
5
|
-
*/
|
|
6
|
-
export declare const registryFor: (name: string, service: string, account: string, key: string, algorithm: string, store?: string) => string;
|
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
import { writeFileSync, mkdtempSync } from 'node:fs';
|
|
2
|
-
import { tmpdir } from 'node:os';
|
|
3
|
-
import { join } from 'node:path';
|
|
4
|
-
import { fileURLToPath } from 'node:url';
|
|
5
|
-
/**
|
|
6
|
-
* The host's registry bound reads to bind a declared signer name to a program: head's own signer, started by this node,
|
|
7
|
-
* told the keychain service, the account its secret is kept under, the key it signs as, the algorithm and the store this
|
|
8
|
-
* host keeps secrets in. It holds no secret; it lives beside nothing a repository ships.
|
|
9
|
-
*/
|
|
10
|
-
export const registryFor = (name, service, account, key, algorithm, store = 'keychain') => {
|
|
11
|
-
const at = join(mkdtempSync(join(tmpdir(), 'head-signers-')), 'signers.json');
|
|
12
|
-
writeFileSync(at, JSON.stringify({ [name]: { kind: 'command', executable: process.execPath, args: [fileURLToPath(new URL(`../signer${import.meta.url.slice(import.meta.url.lastIndexOf('.'))}`, import.meta.url)), service, account, key, algorithm, store] } }));
|
|
13
|
-
return at;
|
|
14
|
-
};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
package/dist/src/ports/spare.js
DELETED
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExtension) || function (path, preserveJsx) {
|
|
2
|
-
if (typeof path === "string" && /^\.\.?\//.test(path)) {
|
|
3
|
-
return path.replace(/\.(tsx)$|((?:\.d)?)((?:\.[^./]+?)?)\.([cm]?)ts$/i, function (m, tsx, d, ext, cm) {
|
|
4
|
-
return tsx ? preserveJsx ? ".jsx" : ".js" : d && (!ext || !cm) ? m : (d + ext + "." + cm.toLowerCase() + "js");
|
|
5
|
-
});
|
|
6
|
-
}
|
|
7
|
-
return path;
|
|
8
|
-
};
|
|
9
|
-
import { readFileSync } from 'node:fs';
|
|
10
|
-
import { Socket } from 'node:net';
|
|
11
|
-
import { pathToFileURL } from 'node:url';
|
|
12
|
-
/**
|
|
13
|
-
* A process started before it is asked: it loads what the program it will run imports, says it is ready on its fourth
|
|
14
|
-
* pipe, and waits there for one ask, the folder, the arguments and the variables a process started then would have been
|
|
15
|
-
* given. It takes them as its own, closes that pipe and runs the program once, as that process would have. An ask that
|
|
16
|
-
* never comes ends it.
|
|
17
|
-
*/
|
|
18
|
-
const [program] = process.argv.slice(2);
|
|
19
|
-
const url = pathToFileURL(program), pipe = new Socket({ fd: 3, readable: true, writable: true });
|
|
20
|
-
const asked = new Promise((resolve) => { let text = ''; pipe.setEncoding('utf8').on('data', (chunk) => { text += chunk; }).on('error', () => undefined).on('close', () => resolve(text)); });
|
|
21
|
-
await Promise.all([...readFileSync(program, 'utf8').matchAll(/(?:^|[;\n])\s*import\s*(?:(?:[^'";]*?)\s*from\s*)?['"](\.{1,2}\/[^'"]+)['"]/g)].map(([, path]) => import(__rewriteRelativeImportExtension(new URL(path, url).href)).catch(() => undefined)));
|
|
22
|
-
pipe.write('ready\n');
|
|
23
|
-
const text = await asked;
|
|
24
|
-
if (!text)
|
|
25
|
-
process.exit(0);
|
|
26
|
-
const { cwd, args, env } = JSON.parse(text);
|
|
27
|
-
for (const name of Object.keys(process.env))
|
|
28
|
-
if (!(name in env))
|
|
29
|
-
delete process.env[name];
|
|
30
|
-
Object.assign(process.env, env);
|
|
31
|
-
process.chdir(cwd);
|
|
32
|
-
process.argv = [process.argv[0], program, ...args];
|
|
33
|
-
await import(__rewriteRelativeImportExtension(url.href));
|
package/dist/src/ports/warm.d.ts
DELETED
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
import type { Bounds, Printed } from './process.ts';
|
|
2
|
-
/** How many processes head keeps started ahead of each program while it answers a call, and the variables node reads as it starts: a process started under other values of them is never asked. */
|
|
3
|
-
export declare const warmth: {
|
|
4
|
-
spares: number;
|
|
5
|
-
boot: readonly string[];
|
|
6
|
-
};
|
|
7
|
-
/** Keeps as many processes started ahead of a program as head's lock says; resolved when each has loaded what it runs. */
|
|
8
|
-
export declare const kept: (program: string) => Promise<unknown>;
|
|
9
|
-
/** Every process still waiting to be asked is stopped: head keeps none past the call it started them for. */
|
|
10
|
-
export declare const cooled: () => void;
|
|
11
|
-
/**
|
|
12
|
-
* A program run in a process started ahead under the same starting variables, handed the folder, arguments and
|
|
13
|
-
* variables a process started now would get, and watched within the same bounds; undefined when none is held ready,
|
|
14
|
-
* so the caller starts one as before and never waits on one still loading. Each process answers one ask and ends.
|
|
15
|
-
*/
|
|
16
|
-
export declare const warmed: (program: string, args: readonly string[], at: {
|
|
17
|
-
readonly cwd: string;
|
|
18
|
-
readonly env: NodeJS.ProcessEnv;
|
|
19
|
-
}, bounds: Bounds) => Promise<Printed> | undefined;
|
package/dist/src/ports/warm.js
DELETED
|
@@ -1,45 +0,0 @@
|
|
|
1
|
-
import { fileURLToPath } from 'node:url';
|
|
2
|
-
import { grouped, watched } from "./process.js";
|
|
3
|
-
/** How many processes head keeps started ahead of each program while it answers a call, and the variables node reads as it starts: a process started under other values of them is never asked. */
|
|
4
|
-
export const warmth = { spares: 0, boot: [] };
|
|
5
|
-
const spares = new Map();
|
|
6
|
-
const alive = (one) => one.child.exitCode === null && one.child.signalCode === null;
|
|
7
|
-
const entry = fileURLToPath(new URL(`./spare${import.meta.url.slice(import.meta.url.lastIndexOf('.'))}`, import.meta.url));
|
|
8
|
-
const bootOf = (env) => JSON.stringify(Object.entries(env).filter(([name]) => warmth.boot.some((p) => name.startsWith(p))).sort(([a], [b]) => (a < b ? -1 : 1)));
|
|
9
|
-
const stop = (one) => { try {
|
|
10
|
-
process.kill(-one.child.pid, 'SIGKILL');
|
|
11
|
-
}
|
|
12
|
-
catch {
|
|
13
|
-
one.child.kill('SIGKILL');
|
|
14
|
-
} };
|
|
15
|
-
const started = (program) => {
|
|
16
|
-
const env = { ...process.env }, child = grouped(process.execPath, [entry, program], { env }, 4);
|
|
17
|
-
child.stdin.on('error', () => undefined).end('');
|
|
18
|
-
const spare = { child, boot: bootOf(env), ready: new Promise((resolve) => { child.stdio[3].on('error', () => undefined).once('data', () => { spare.loaded = true; resolve(); }); child.once('close', () => resolve()); }), loaded: false };
|
|
19
|
-
return spare;
|
|
20
|
-
};
|
|
21
|
-
/** Keeps as many processes started ahead of a program as head's lock says; resolved when each has loaded what it runs. */
|
|
22
|
-
export const kept = (program) => {
|
|
23
|
-
const live = (spares.get(program) ?? []).filter(alive);
|
|
24
|
-
while (live.length < warmth.spares)
|
|
25
|
-
live.push(started(program));
|
|
26
|
-
spares.set(program, live);
|
|
27
|
-
return Promise.all(live.map((one) => one.ready));
|
|
28
|
-
};
|
|
29
|
-
/** Every process still waiting to be asked is stopped: head keeps none past the call it started them for. */
|
|
30
|
-
export const cooled = () => { for (const one of [...spares.values()].flat())
|
|
31
|
-
stop(one); spares.clear(); };
|
|
32
|
-
process.once('exit', cooled);
|
|
33
|
-
/**
|
|
34
|
-
* A program run in a process started ahead under the same starting variables, handed the folder, arguments and
|
|
35
|
-
* variables a process started now would get, and watched within the same bounds; undefined when none is held ready,
|
|
36
|
-
* so the caller starts one as before and never waits on one still loading. Each process answers one ask and ends.
|
|
37
|
-
*/
|
|
38
|
-
export const warmed = (program, args, at, bounds) => {
|
|
39
|
-
const live = spares.get(program) ?? [], index = live.findIndex((one) => one.loaded && alive(one) && one.boot === bootOf(at.env));
|
|
40
|
-
const spare = index < 0 ? undefined : live.splice(index, 1)[0];
|
|
41
|
-
if (spare === undefined)
|
|
42
|
-
return undefined;
|
|
43
|
-
spare.child.stdio[3].end(`${JSON.stringify({ cwd: at.cwd, args, env: at.env })}\n`);
|
|
44
|
-
return watched(spare.child, bounds).finally(() => { void kept(program); });
|
|
45
|
-
};
|