@prestack/thread 0.1.4 → 0.1.6
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/CHANGELOG.md +553 -0
- package/dist/core/attach.js +11 -5
- package/dist/core/attach.ts +9 -5
- package/dist/core/descend.js +4 -4
- package/dist/core/descend.ts +4 -4
- package/dist/core/generators/commit.d.ts +1 -0
- package/dist/core/generators/commit.js +150 -0
- package/dist/core/generators/commit.ts +157 -0
- package/dist/core/init.d.ts +1 -0
- package/dist/core/init.js +3 -1
- package/dist/core/init.ts +4 -1
- package/dist/core/scope.d.ts +1 -0
- package/dist/core/scope.js +1 -0
- package/dist/core/scope.ts +2 -0
- package/dist/derivation/attach.js +2 -1
- package/dist/derivation/attach.ts +2 -1
- package/dist/integrations/attach.js +5 -3
- package/dist/integrations/attach.ts +5 -4
- package/dist/integrity/attach.js +6 -5
- package/dist/integrity/attach.ts +6 -5
- package/dist/integrity/generators/compiler.js +6 -32
- package/dist/integrity/generators/compiler.ts +8 -35
- package/dist/integrity/generators/gate.js +7 -1
- package/dist/integrity/generators/gate.ts +8 -1
- package/dist/integrity/slopnator.js +1 -1
- package/dist/integrity/slopnator.ts +1 -1
- package/dist/knowledge/attach.js +3 -2
- package/dist/knowledge/attach.ts +3 -2
- package/dist/operations/attach.js +7 -6
- package/dist/operations/attach.ts +7 -6
- package/package.json +1 -1
package/dist/core/attach.js
CHANGED
|
@@ -12,10 +12,10 @@ import { createRequire } from 'node:module';
|
|
|
12
12
|
import { join, relative, resolve } from 'node:path';
|
|
13
13
|
import { stdout } from 'node:process';
|
|
14
14
|
import { LISTS } from './descend.js';
|
|
15
|
-
import { ROOT, TREE, root as rooted } from './init.js';
|
|
15
|
+
import { ROOT, SHAPED, TREE, root as rooted } from './init.js';
|
|
16
16
|
import { BESIDE } from './invoked.js';
|
|
17
17
|
import { STATED, holdsRow, listBounds, quoted, reconciled, rowsIn, withRow, withoutRow } from './rows.js';
|
|
18
|
-
import { idOf, ours as isOurs } from './scope.js';
|
|
18
|
+
import { idOf, installed, ours as isOurs } from './scope.js';
|
|
19
19
|
const SHAPE = /export interface [A-Za-z0-9_]+ extends Threaded \{[\s\S]*?\n\}\n/;
|
|
20
20
|
const MEMBERS = 'MEMBERS';
|
|
21
21
|
const DOOR = '/attach';
|
|
@@ -178,7 +178,6 @@ export async function attach(into, name, touched = new Set(), under = []) {
|
|
|
178
178
|
const ours = [...one.declares.matchAll(/\bid: '([^']+)'/g)].map((found) => found[1]);
|
|
179
179
|
const mine = declares !== undefined &&
|
|
180
180
|
(holdsRow(held, declares, one.id) || ours.some((id) => id !== undefined && holdsRow(held, declares, id)));
|
|
181
|
-
const installed = (at) => at.startsWith('node_modules/') && isOurs(at.split('/').slice(1, 3).join('/'));
|
|
182
181
|
const pathIn = (source) => {
|
|
183
182
|
const listed = `const HELD = {\n declares: [\n${source}\n ],\n}\n`;
|
|
184
183
|
const span = listBounds(listed, 'declares');
|
|
@@ -485,7 +484,7 @@ export async function detach(into, name) {
|
|
|
485
484
|
return [...touched].sort();
|
|
486
485
|
}
|
|
487
486
|
const DESCRIBES = 'The package every tree installs first: the descent that reads a tree, the gate that checks it, and the generators every tree runs.';
|
|
488
|
-
const CORE_SOURCE = `import type { Threaded } from '
|
|
487
|
+
const CORE_SOURCE = `import type { Threaded } from '${SHAPED}'
|
|
489
488
|
|
|
490
489
|
export const CORE: Threaded = {
|
|
491
490
|
id: 'core',
|
|
@@ -509,7 +508,14 @@ const SEEDED = {
|
|
|
509
508
|
' },',
|
|
510
509
|
].join('\n'),
|
|
511
510
|
};
|
|
512
|
-
const SHIPPED = [
|
|
511
|
+
const SHIPPED = [
|
|
512
|
+
{
|
|
513
|
+
into: 'derivation.ts',
|
|
514
|
+
list: 'GENERATORS',
|
|
515
|
+
id: 'commit',
|
|
516
|
+
source: " {\n id: 'commit',\n at: 'node_modules/@prestack/thread/dist/core/generators/commit.js',\n describes: 'Commits every change in this app, so a create or an edit leaves a mark in version control without anyone typing a git command. Unlike `cl_commit`, which runs real git to record a release on the machine the command runs on, this writes the commit in the node the command already runs in, so it works inside the permission fence an attached app runs behind, where no program may be started. It stages what changed, records it under the message it is given, and starts a repository the first time there is none. Pushing and undoing stay deliberate.',\n version: 1,\n by: 'thread commit',\n },",
|
|
517
|
+
},
|
|
518
|
+
];
|
|
513
519
|
export const ATTACH = {
|
|
514
520
|
attaches: SEEDED.attaches,
|
|
515
521
|
id: SEEDED.id,
|
package/dist/core/attach.ts
CHANGED
|
@@ -5,10 +5,10 @@ import { join, relative, resolve } from 'node:path'
|
|
|
5
5
|
import { stdout } from 'node:process'
|
|
6
6
|
|
|
7
7
|
import { LISTS } from './descend.ts'
|
|
8
|
-
import { ROOT, TREE, root as rooted } from './init.ts'
|
|
8
|
+
import { ROOT, SHAPED, TREE, root as rooted } from './init.ts'
|
|
9
9
|
import { BESIDE } from './invoked.ts'
|
|
10
10
|
import { STATED, holdsRow, listBounds, quoted, reconciled, rowsIn, withRow, withoutRow } from './rows.ts'
|
|
11
|
-
import { idOf, ours as isOurs } from './scope.ts'
|
|
11
|
+
import { idOf, installed, ours as isOurs } from './scope.ts'
|
|
12
12
|
|
|
13
13
|
const SHAPE = /export interface [A-Za-z0-9_]+ extends Threaded \{[\s\S]*?\n\}\n/
|
|
14
14
|
|
|
@@ -230,8 +230,6 @@ export async function attach(
|
|
|
230
230
|
declares !== undefined &&
|
|
231
231
|
(holdsRow(held, declares, one.id) || ours.some((id) => id !== undefined && holdsRow(held, declares, id)))
|
|
232
232
|
|
|
233
|
-
const installed = (at: string): boolean => at.startsWith('node_modules/') && isOurs(at.split('/').slice(1, 3).join('/'))
|
|
234
|
-
|
|
235
233
|
const pathIn = (source: string): string | undefined => {
|
|
236
234
|
const listed = `const HELD = {\n declares: [\n${source}\n ],\n}\n`
|
|
237
235
|
const span = listBounds(listed, 'declares')
|
|
@@ -540,7 +538,7 @@ export async function detach(into: string, name: string): Promise<readonly strin
|
|
|
540
538
|
const DESCRIBES =
|
|
541
539
|
'The package every tree installs first: the descent that reads a tree, the gate that checks it, and the generators every tree runs.'
|
|
542
540
|
|
|
543
|
-
const CORE_SOURCE = `import type { Threaded } from '
|
|
541
|
+
const CORE_SOURCE = `import type { Threaded } from '${SHAPED}'
|
|
544
542
|
|
|
545
543
|
export const CORE: Threaded = {
|
|
546
544
|
id: 'core',
|
|
@@ -567,6 +565,12 @@ const SEEDED = {
|
|
|
567
565
|
}
|
|
568
566
|
|
|
569
567
|
const SHIPPED: readonly { readonly into: string; readonly list: string; readonly id: string; readonly source: string }[] = [
|
|
568
|
+
{
|
|
569
|
+
into: 'derivation.ts',
|
|
570
|
+
list: 'GENERATORS',
|
|
571
|
+
id: 'commit',
|
|
572
|
+
source: " {\n id: 'commit',\n at: 'node_modules/@prestack/thread/dist/core/generators/commit.js',\n describes: 'Commits every change in this app, so a create or an edit leaves a mark in version control without anyone typing a git command. Unlike `cl_commit`, which runs real git to record a release on the machine the command runs on, this writes the commit in the node the command already runs in, so it works inside the permission fence an attached app runs behind, where no program may be started. It stages what changed, records it under the message it is given, and starts a repository the first time there is none. Pushing and undoing stay deliberate.',\n version: 1,\n by: 'thread commit',\n },",
|
|
573
|
+
},
|
|
570
574
|
]
|
|
571
575
|
export const ATTACH: Attaching = {
|
|
572
576
|
attaches: SEEDED.attaches,
|
package/dist/core/descend.js
CHANGED
|
@@ -2,8 +2,8 @@ import { starts } from '../integrations/cli.js';
|
|
|
2
2
|
import { attached, beyond, existsSync, here as answering, kind, readdirSync, readFileSync, realpathSync, statSync, writeFileSync } from '../integrations/disk.js';
|
|
3
3
|
import { dirname, join, relative, resolve } from 'node:path';
|
|
4
4
|
import ts from 'typescript';
|
|
5
|
-
import { ignored } from './init.js';
|
|
6
|
-
import {
|
|
5
|
+
import { SHAPED, ignored } from './init.js';
|
|
6
|
+
import { ORGANISATION, installed } from './scope.js';
|
|
7
7
|
import { findTree } from './tree.js';
|
|
8
8
|
const BUILT = new Set(ignored()
|
|
9
9
|
.split('\n')
|
|
@@ -780,7 +780,7 @@ export function descend() {
|
|
|
780
780
|
broken.push({
|
|
781
781
|
file: THREAD_AT,
|
|
782
782
|
detail: 'the root does not resolve `Threaded`, so nothing here can be recognised as a declaration and every file reads as unthreaded',
|
|
783
|
-
instead: `import \`Threaded\` from \`${
|
|
783
|
+
instead: `import \`Threaded\` from \`${SHAPED}\` in the root, or run \`thread init\`. Without the shape there is nothing to hold anything to`,
|
|
784
784
|
});
|
|
785
785
|
passed = { threaded, mentioned, derived, stated, coined, broken, findings, declarations, rows, sources: sources() };
|
|
786
786
|
return passed;
|
|
@@ -849,7 +849,7 @@ export function descend() {
|
|
|
849
849
|
return;
|
|
850
850
|
}
|
|
851
851
|
const target = resolve(ROOT, row.at);
|
|
852
|
-
if (unkept(row.at)) {
|
|
852
|
+
if (!installed(row.at) && unkept(row.at)) {
|
|
853
853
|
findings.push({
|
|
854
854
|
file: at,
|
|
855
855
|
detail: `\`${names(row)}\` names \`${row.at}\`, which git ignores, so it is on this machine and in no clone of it`,
|
package/dist/core/descend.ts
CHANGED
|
@@ -5,8 +5,8 @@ import { dirname, join, relative, resolve } from 'node:path'
|
|
|
5
5
|
import ts from 'typescript'
|
|
6
6
|
|
|
7
7
|
import type { Finding } from './finding.ts'
|
|
8
|
-
import { ignored } from './init.ts'
|
|
9
|
-
import {
|
|
8
|
+
import { SHAPED, ignored } from './init.ts'
|
|
9
|
+
import { ORGANISATION, installed } from './scope.ts'
|
|
10
10
|
import { findTree } from './tree.ts'
|
|
11
11
|
|
|
12
12
|
const BUILT: ReadonlySet<string> = new Set(
|
|
@@ -869,7 +869,7 @@ export function descend(): Pass {
|
|
|
869
869
|
broken.push({
|
|
870
870
|
file: THREAD_AT,
|
|
871
871
|
detail: 'the root does not resolve `Threaded`, so nothing here can be recognised as a declaration and every file reads as unthreaded',
|
|
872
|
-
instead: `import \`Threaded\` from \`${
|
|
872
|
+
instead: `import \`Threaded\` from \`${SHAPED}\` in the root, or run \`thread init\`. Without the shape there is nothing to hold anything to`,
|
|
873
873
|
})
|
|
874
874
|
passed = { threaded, mentioned, derived, stated, coined, broken, findings, declarations, rows, sources: sources() }
|
|
875
875
|
return passed
|
|
@@ -940,7 +940,7 @@ export function descend(): Pass {
|
|
|
940
940
|
}
|
|
941
941
|
|
|
942
942
|
const target = resolve(ROOT, row.at)
|
|
943
|
-
if (unkept(row.at)) {
|
|
943
|
+
if (!installed(row.at) && unkept(row.at)) {
|
|
944
944
|
findings.push({
|
|
945
945
|
file: at,
|
|
946
946
|
detail: `\`${names(row)}\` names \`${row.at}\`, which git ignores, so it is on this machine and in no clone of it`,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { deflateSync, inflateSync } from 'node:zlib';
|
|
3
|
+
import { existsSync, mkdirSync, readFileSync, readdirSync, statSync, writeFileSync } from 'node:fs';
|
|
4
|
+
import { join, relative, sep } from 'node:path';
|
|
5
|
+
import { argv, cwd, stdout, stderr } from 'node:process';
|
|
6
|
+
const dir = cwd();
|
|
7
|
+
const message = (argv[2] ?? 'thread').slice(0, 400);
|
|
8
|
+
const NL = String.fromCharCode(10);
|
|
9
|
+
const NUL = String.fromCharCode(0);
|
|
10
|
+
const SLASH = String.fromCharCode(47);
|
|
11
|
+
const matches = (pat, name) => {
|
|
12
|
+
const parts = pat.split('*');
|
|
13
|
+
if (parts.length === 1)
|
|
14
|
+
return name === pat;
|
|
15
|
+
const first = parts[0];
|
|
16
|
+
if (!name.startsWith(first))
|
|
17
|
+
return false;
|
|
18
|
+
let at = first.length;
|
|
19
|
+
for (let k = 1; k < parts.length - 1; k += 1) {
|
|
20
|
+
const found = name.indexOf(parts[k], at);
|
|
21
|
+
if (found === -1)
|
|
22
|
+
return false;
|
|
23
|
+
at = found + parts[k].length;
|
|
24
|
+
}
|
|
25
|
+
const last = parts[parts.length - 1];
|
|
26
|
+
return name.length - last.length >= at && name.endsWith(last);
|
|
27
|
+
};
|
|
28
|
+
let text = '';
|
|
29
|
+
try {
|
|
30
|
+
text = readFileSync(join(dir, '.gitignore'), 'utf8');
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
text = '';
|
|
34
|
+
}
|
|
35
|
+
const patterns = [];
|
|
36
|
+
for (const raw of text.split(NL)) {
|
|
37
|
+
let line = raw.trim();
|
|
38
|
+
if (line === '' || line.startsWith('#'))
|
|
39
|
+
continue;
|
|
40
|
+
const negate = line.startsWith('!');
|
|
41
|
+
if (negate)
|
|
42
|
+
line = line.slice(1);
|
|
43
|
+
const dirOnly = line.endsWith(SLASH);
|
|
44
|
+
if (dirOnly)
|
|
45
|
+
line = line.slice(0, -1);
|
|
46
|
+
let anchored = line.startsWith(SLASH);
|
|
47
|
+
if (anchored)
|
|
48
|
+
line = line.slice(1);
|
|
49
|
+
if (line.includes(SLASH))
|
|
50
|
+
anchored = true;
|
|
51
|
+
if (line !== '')
|
|
52
|
+
patterns.push({ negate, dirOnly, anchored, pat: line });
|
|
53
|
+
}
|
|
54
|
+
const ignored = (rel, name, isDir) => {
|
|
55
|
+
if (name === '.git')
|
|
56
|
+
return true;
|
|
57
|
+
let hit = false;
|
|
58
|
+
for (const one of patterns) {
|
|
59
|
+
if (one.dirOnly && !isDir)
|
|
60
|
+
continue;
|
|
61
|
+
if (matches(one.pat, one.anchored ? rel : name))
|
|
62
|
+
hit = !one.negate;
|
|
63
|
+
}
|
|
64
|
+
return hit;
|
|
65
|
+
};
|
|
66
|
+
const BRANCH = ['refs', 'heads', 'main'].join(SLASH);
|
|
67
|
+
const store = (kind, body) => {
|
|
68
|
+
const whole = Buffer.concat([Buffer.from(kind + ' ' + String(body.length) + NUL), body]);
|
|
69
|
+
const sha = createHash('sha1').update(whole).digest('hex');
|
|
70
|
+
const at = join(dir, '.git', 'objects', sha.slice(0, 2), sha.slice(2));
|
|
71
|
+
if (!existsSync(at)) {
|
|
72
|
+
mkdirSync(join(dir, '.git', 'objects', sha.slice(0, 2)), { recursive: true });
|
|
73
|
+
writeFileSync(at, deflateSync(whole));
|
|
74
|
+
}
|
|
75
|
+
return sha;
|
|
76
|
+
};
|
|
77
|
+
const tree = (at) => {
|
|
78
|
+
const entries = [];
|
|
79
|
+
for (const name of readdirSync(at)) {
|
|
80
|
+
const full = join(at, name);
|
|
81
|
+
const stat = statSync(full);
|
|
82
|
+
const rel = relative(dir, full).split(sep).join(SLASH);
|
|
83
|
+
if (ignored(rel, name, stat.isDirectory()))
|
|
84
|
+
continue;
|
|
85
|
+
if (stat.isDirectory()) {
|
|
86
|
+
const sub = tree(full);
|
|
87
|
+
if (sub !== '')
|
|
88
|
+
entries.push({ mode: '40000', name, sha: sub });
|
|
89
|
+
}
|
|
90
|
+
else if (stat.isFile()) {
|
|
91
|
+
const sha = store('blob', readFileSync(full));
|
|
92
|
+
entries.push({ mode: (stat.mode & 0o111) !== 0 ? '100755' : '100644', name, sha });
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
if (entries.length === 0)
|
|
96
|
+
return '';
|
|
97
|
+
const key = (one) => one.name + (one.mode === '40000' ? SLASH : '');
|
|
98
|
+
entries.sort((one, two) => (key(one) < key(two) ? -1 : key(one) > key(two) ? 1 : 0));
|
|
99
|
+
const body = Buffer.concat(entries.map((one) => Buffer.concat([Buffer.from(one.mode + ' ' + one.name + NUL), Buffer.from(one.sha, 'hex')])));
|
|
100
|
+
return store('tree', body);
|
|
101
|
+
};
|
|
102
|
+
const treeOf = (sha) => {
|
|
103
|
+
const at = join(dir, '.git', 'objects', sha.slice(0, 2), sha.slice(2));
|
|
104
|
+
const raw = inflateSync(readFileSync(at));
|
|
105
|
+
const body = raw.slice(raw.indexOf(0) + 1).toString('utf8');
|
|
106
|
+
const first = body.split(NL)[0] ?? '';
|
|
107
|
+
return first.startsWith('tree ') ? first.slice(5) : '';
|
|
108
|
+
};
|
|
109
|
+
const record = () => {
|
|
110
|
+
const gitDir = join(dir, '.git');
|
|
111
|
+
if (!existsSync(join(gitDir, 'HEAD'))) {
|
|
112
|
+
mkdirSync(join(gitDir, 'objects'), { recursive: true });
|
|
113
|
+
mkdirSync(join(gitDir, 'refs', 'heads'), { recursive: true });
|
|
114
|
+
writeFileSync(join(gitDir, 'HEAD'), 'ref: ' + BRANCH + NL);
|
|
115
|
+
}
|
|
116
|
+
const head = readFileSync(join(gitDir, 'HEAD'), 'utf8').trim();
|
|
117
|
+
const ref = head.startsWith('ref: ') ? head.slice(5) : BRANCH;
|
|
118
|
+
const refAt = join(gitDir, ref);
|
|
119
|
+
const parent = existsSync(refAt) ? readFileSync(refAt, 'utf8').trim() : '';
|
|
120
|
+
const root = tree(dir);
|
|
121
|
+
if (root === '') {
|
|
122
|
+
stdout.write('commit: nothing to commit' + NL);
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
if (parent !== '' && treeOf(parent) === root) {
|
|
126
|
+
stdout.write('commit: nothing changed' + NL);
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
const when = String(Math.floor(Date.now() / 1000)) + ' +0000';
|
|
130
|
+
const who = 'thread <thread@prestack> ' + when;
|
|
131
|
+
const body = [
|
|
132
|
+
'tree ' + root,
|
|
133
|
+
...(parent === '' ? [] : ['parent ' + parent]),
|
|
134
|
+
'author ' + who,
|
|
135
|
+
'committer ' + who,
|
|
136
|
+
'',
|
|
137
|
+
message,
|
|
138
|
+
'',
|
|
139
|
+
].join(NL);
|
|
140
|
+
const sha = store('commit', Buffer.from(body));
|
|
141
|
+
writeFileSync(refAt, sha + NL);
|
|
142
|
+
stdout.write('commit: ' + sha.slice(0, 8) + ' on ' + (ref.split(SLASH).pop() ?? 'main') + NL);
|
|
143
|
+
};
|
|
144
|
+
try {
|
|
145
|
+
record();
|
|
146
|
+
}
|
|
147
|
+
catch (thrown) {
|
|
148
|
+
stderr.write('commit: ' + (thrown instanceof Error ? thrown.message : String(thrown)) + NL);
|
|
149
|
+
process.exit(1);
|
|
150
|
+
}
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto'
|
|
2
|
+
import { deflateSync, inflateSync } from 'node:zlib'
|
|
3
|
+
import { existsSync, mkdirSync, readFileSync, readdirSync, statSync, writeFileSync } from 'node:fs'
|
|
4
|
+
import { join, relative, sep } from 'node:path'
|
|
5
|
+
import { argv, cwd, stdout, stderr } from 'node:process'
|
|
6
|
+
|
|
7
|
+
const dir = cwd()
|
|
8
|
+
const message = (argv[2] ?? 'thread').slice(0, 400)
|
|
9
|
+
const NL = String.fromCharCode(10)
|
|
10
|
+
const NUL = String.fromCharCode(0)
|
|
11
|
+
const SLASH = String.fromCharCode(47)
|
|
12
|
+
const matches = (pat: string, name: string): boolean => {
|
|
13
|
+
const parts = pat.split('*')
|
|
14
|
+
if (parts.length === 1) return name === pat
|
|
15
|
+
const first = parts[0]
|
|
16
|
+
if (!name.startsWith(first)) return false
|
|
17
|
+
let at = first.length
|
|
18
|
+
for (let k = 1; k < parts.length - 1; k += 1) {
|
|
19
|
+
const found = name.indexOf(parts[k], at)
|
|
20
|
+
if (found === -1) return false
|
|
21
|
+
at = found + parts[k].length
|
|
22
|
+
}
|
|
23
|
+
const last = parts[parts.length - 1]
|
|
24
|
+
return name.length - last.length >= at && name.endsWith(last)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
interface Ignore {
|
|
28
|
+
readonly negate: boolean
|
|
29
|
+
readonly dirOnly: boolean
|
|
30
|
+
readonly anchored: boolean
|
|
31
|
+
readonly pat: string
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
let text = ''
|
|
35
|
+
try {
|
|
36
|
+
text = readFileSync(join(dir, '.gitignore'), 'utf8')
|
|
37
|
+
} catch {
|
|
38
|
+
text = ''
|
|
39
|
+
}
|
|
40
|
+
const patterns: Ignore[] = []
|
|
41
|
+
for (const raw of text.split(NL)) {
|
|
42
|
+
let line = raw.trim()
|
|
43
|
+
if (line === '' || line.startsWith('#')) continue
|
|
44
|
+
const negate = line.startsWith('!')
|
|
45
|
+
if (negate) line = line.slice(1)
|
|
46
|
+
const dirOnly = line.endsWith(SLASH)
|
|
47
|
+
if (dirOnly) line = line.slice(0, -1)
|
|
48
|
+
let anchored = line.startsWith(SLASH)
|
|
49
|
+
if (anchored) line = line.slice(1)
|
|
50
|
+
if (line.includes(SLASH)) anchored = true
|
|
51
|
+
if (line !== '') patterns.push({ negate, dirOnly, anchored, pat: line })
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const ignored = (rel: string, name: string, isDir: boolean): boolean => {
|
|
55
|
+
if (name === '.git') return true
|
|
56
|
+
let hit = false
|
|
57
|
+
for (const one of patterns) {
|
|
58
|
+
if (one.dirOnly && !isDir) continue
|
|
59
|
+
if (matches(one.pat, one.anchored ? rel : name)) hit = !one.negate
|
|
60
|
+
}
|
|
61
|
+
return hit
|
|
62
|
+
}
|
|
63
|
+
const BRANCH = ['refs', 'heads', 'main'].join(SLASH)
|
|
64
|
+
|
|
65
|
+
interface Entry {
|
|
66
|
+
readonly mode: string
|
|
67
|
+
readonly name: string
|
|
68
|
+
readonly sha: string
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const store = (kind: string, body: Buffer): string => {
|
|
72
|
+
const whole = Buffer.concat([Buffer.from(kind + ' ' + String(body.length) + NUL), body])
|
|
73
|
+
const sha = createHash('sha1').update(whole).digest('hex')
|
|
74
|
+
const at = join(dir, '.git', 'objects', sha.slice(0, 2), sha.slice(2))
|
|
75
|
+
if (!existsSync(at)) {
|
|
76
|
+
mkdirSync(join(dir, '.git', 'objects', sha.slice(0, 2)), { recursive: true })
|
|
77
|
+
writeFileSync(at, deflateSync(whole))
|
|
78
|
+
}
|
|
79
|
+
return sha
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const tree = (at: string): string => {
|
|
83
|
+
const entries: Entry[] = []
|
|
84
|
+
for (const name of readdirSync(at)) {
|
|
85
|
+
const full = join(at, name)
|
|
86
|
+
const stat = statSync(full)
|
|
87
|
+
const rel = relative(dir, full).split(sep).join(SLASH)
|
|
88
|
+
if (ignored(rel, name, stat.isDirectory())) continue
|
|
89
|
+
if (stat.isDirectory()) {
|
|
90
|
+
const sub = tree(full)
|
|
91
|
+
if (sub !== '') entries.push({ mode: '40000', name, sha: sub })
|
|
92
|
+
} else if (stat.isFile()) {
|
|
93
|
+
const sha = store('blob', readFileSync(full))
|
|
94
|
+
entries.push({ mode: (stat.mode & 0o111) !== 0 ? '100755' : '100644', name, sha })
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
if (entries.length === 0) return ''
|
|
98
|
+
const key = (one: Entry): string => one.name + (one.mode === '40000' ? SLASH : '')
|
|
99
|
+
entries.sort((one, two) => (key(one) < key(two) ? -1 : key(one) > key(two) ? 1 : 0))
|
|
100
|
+
const body = Buffer.concat(
|
|
101
|
+
entries.map((one) => Buffer.concat([Buffer.from(one.mode + ' ' + one.name + NUL), Buffer.from(one.sha, 'hex')])),
|
|
102
|
+
)
|
|
103
|
+
return store('tree', body)
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const treeOf = (sha: string): string => {
|
|
107
|
+
const at = join(dir, '.git', 'objects', sha.slice(0, 2), sha.slice(2))
|
|
108
|
+
const raw = inflateSync(readFileSync(at))
|
|
109
|
+
const body = raw.slice(raw.indexOf(0) + 1).toString('utf8')
|
|
110
|
+
const first = body.split(NL)[0] ?? ''
|
|
111
|
+
return first.startsWith('tree ') ? first.slice(5) : ''
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const record = (): void => {
|
|
115
|
+
const gitDir = join(dir, '.git')
|
|
116
|
+
if (!existsSync(join(gitDir, 'HEAD'))) {
|
|
117
|
+
mkdirSync(join(gitDir, 'objects'), { recursive: true })
|
|
118
|
+
mkdirSync(join(gitDir, 'refs', 'heads'), { recursive: true })
|
|
119
|
+
writeFileSync(join(gitDir, 'HEAD'), 'ref: ' + BRANCH + NL)
|
|
120
|
+
}
|
|
121
|
+
const head = readFileSync(join(gitDir, 'HEAD'), 'utf8').trim()
|
|
122
|
+
const ref = head.startsWith('ref: ') ? head.slice(5) : BRANCH
|
|
123
|
+
const refAt = join(gitDir, ref)
|
|
124
|
+
const parent = existsSync(refAt) ? readFileSync(refAt, 'utf8').trim() : ''
|
|
125
|
+
|
|
126
|
+
const root = tree(dir)
|
|
127
|
+
if (root === '') {
|
|
128
|
+
stdout.write('commit: nothing to commit' + NL)
|
|
129
|
+
return
|
|
130
|
+
}
|
|
131
|
+
if (parent !== '' && treeOf(parent) === root) {
|
|
132
|
+
stdout.write('commit: nothing changed' + NL)
|
|
133
|
+
return
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const when = String(Math.floor(Date.now() / 1000)) + ' +0000'
|
|
137
|
+
const who = 'thread <thread@prestack> ' + when
|
|
138
|
+
const body = [
|
|
139
|
+
'tree ' + root,
|
|
140
|
+
...(parent === '' ? [] : ['parent ' + parent]),
|
|
141
|
+
'author ' + who,
|
|
142
|
+
'committer ' + who,
|
|
143
|
+
'',
|
|
144
|
+
message,
|
|
145
|
+
'',
|
|
146
|
+
].join(NL)
|
|
147
|
+
const sha = store('commit', Buffer.from(body))
|
|
148
|
+
writeFileSync(refAt, sha + NL)
|
|
149
|
+
stdout.write('commit: ' + sha.slice(0, 8) + ' on ' + (ref.split(SLASH).pop() ?? 'main') + NL)
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
try {
|
|
153
|
+
record()
|
|
154
|
+
} catch (thrown) {
|
|
155
|
+
stderr.write('commit: ' + (thrown instanceof Error ? thrown.message : String(thrown)) + NL)
|
|
156
|
+
process.exit(1)
|
|
157
|
+
}
|
package/dist/core/init.d.ts
CHANGED
package/dist/core/init.js
CHANGED
|
@@ -4,8 +4,10 @@ import { createRequire } from 'node:module';
|
|
|
4
4
|
import { join, relative, resolve } from 'node:path';
|
|
5
5
|
import { fileURLToPath } from 'node:url';
|
|
6
6
|
import { quoted } from './rows.js';
|
|
7
|
+
import { NAMED } from './scope.js';
|
|
7
8
|
export const TREE = '_threads';
|
|
8
9
|
export const ROOT = 'thread.ts';
|
|
10
|
+
export const SHAPED = `${NAMED}/core/thread`;
|
|
9
11
|
export class AlreadyThreadedError extends Error {
|
|
10
12
|
at;
|
|
11
13
|
constructor(at) {
|
|
@@ -90,7 +92,7 @@ export function root(into) {
|
|
|
90
92
|
' },',
|
|
91
93
|
]);
|
|
92
94
|
return [
|
|
93
|
-
|
|
95
|
+
`import type { Threaded } from '${SHAPED}'`,
|
|
94
96
|
'',
|
|
95
97
|
'export const THREAD: Threaded = {',
|
|
96
98
|
" id: 'thread',",
|
package/dist/core/init.ts
CHANGED
|
@@ -5,11 +5,14 @@ import { join, relative, resolve } from 'node:path'
|
|
|
5
5
|
import { fileURLToPath } from 'node:url'
|
|
6
6
|
|
|
7
7
|
import { quoted } from './rows.ts'
|
|
8
|
+
import { NAMED } from './scope.ts'
|
|
8
9
|
|
|
9
10
|
|
|
10
11
|
export const TREE = '_threads'
|
|
11
12
|
export const ROOT = 'thread.ts'
|
|
12
13
|
|
|
14
|
+
export const SHAPED = `${NAMED}/core/thread`
|
|
15
|
+
|
|
13
16
|
export class AlreadyThreadedError extends Error {
|
|
14
17
|
readonly at: string
|
|
15
18
|
constructor(at: string) {
|
|
@@ -123,7 +126,7 @@ export function root(into: string): string {
|
|
|
123
126
|
' },',
|
|
124
127
|
])
|
|
125
128
|
return [
|
|
126
|
-
|
|
129
|
+
`import type { Threaded } from '${SHAPED}'`,
|
|
127
130
|
'',
|
|
128
131
|
'export const THREAD: Threaded = {',
|
|
129
132
|
" id: 'thread',",
|
package/dist/core/scope.d.ts
CHANGED
|
@@ -4,3 +4,4 @@ export declare const NAMED = "@prestack/thread";
|
|
|
4
4
|
export declare const named: (id: string) => string;
|
|
5
5
|
export declare const idOf: (name: string) => string | undefined;
|
|
6
6
|
export declare const ours: (name: string) => boolean;
|
|
7
|
+
export declare const installed: (at: string) => boolean;
|
package/dist/core/scope.js
CHANGED
|
@@ -10,3 +10,4 @@ export const idOf = (name) => {
|
|
|
10
10
|
return name.startsWith(HEAD) ? name.slice(HEAD.length) : undefined;
|
|
11
11
|
};
|
|
12
12
|
export const ours = (name) => idOf(name) !== undefined;
|
|
13
|
+
export const installed = (at) => at.startsWith('node_modules/') && ours(at.split('/').slice(1, 3).join('/'));
|
package/dist/core/scope.ts
CHANGED
|
@@ -16,3 +16,5 @@ export const idOf = (name: string): string | undefined => {
|
|
|
16
16
|
}
|
|
17
17
|
|
|
18
18
|
export const ours = (name: string): boolean => idOf(name) !== undefined
|
|
19
|
+
|
|
20
|
+
export const installed = (at: string): boolean => at.startsWith('node_modules/') && ours(at.split('/').slice(1, 3).join('/'))
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
+
import { SHAPED } from '../core/init.js';
|
|
1
2
|
import { quoted } from '../core/rows.js';
|
|
2
3
|
const DESCRIBES = 'What turns a declaration into a file: the rows that state something is to be made, and the generators that make it. They are kept apart so you can read what an app declares without opening the machinery that builds it.';
|
|
3
4
|
const DERIVATION_SOURCE = [
|
|
4
|
-
|
|
5
|
+
`import type { Threaded } from '${SHAPED}'`,
|
|
5
6
|
'',
|
|
6
7
|
'export const DERIVATION: Threaded = {',
|
|
7
8
|
" id: 'derivation',",
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import type { Attaching } from '../core/attach.ts'
|
|
2
2
|
|
|
3
|
+
import { SHAPED } from '../core/init.ts'
|
|
3
4
|
import { quoted } from '../core/rows.ts'
|
|
4
5
|
|
|
5
6
|
const DESCRIBES =
|
|
6
7
|
'What turns a declaration into a file: the rows that state something is to be made, and the generators that make it. They are kept apart so you can read what an app declares without opening the machinery that builds it.'
|
|
7
8
|
|
|
8
9
|
const DERIVATION_SOURCE = [
|
|
9
|
-
|
|
10
|
+
`import type { Threaded } from '${SHAPED}'`,
|
|
10
11
|
'',
|
|
11
12
|
'export const DERIVATION: Threaded = {',
|
|
12
13
|
" id: 'derivation',",
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { SHAPED } from '../core/init.js';
|
|
2
|
+
import { NAMED } from '../core/scope.js';
|
|
1
3
|
const VERSION = 1;
|
|
2
4
|
const DESCRIBES = 'The system that states every place this app reaches and every credential it reaches with. A target is a destination and the prefix on its id is how it is reached; what it accepts is entities if it keeps them, and calls if it is called: an endpoint over https, a subcommand of a program, a method of a package; a disk accepts nothing, because every disk answers the same orders; what it requires is secrets, and the target it is reached through when the place can only be got at by way of another one; where it is goes on the row as its address when anybody may read it, and is one of the secrets when it carries a password. Nothing else in this app names a host, a connection string or an environment variable, so an operation says which place it needs and the rest is worked out from the declaration.';
|
|
3
5
|
const TARGETS = 'Where this app states everything outside itself that it deals with, and every way in. An integration is one outside thing — a vendor, a service, a program on the machine — and nothing is reached that is not part of one; declare it with `thread create integration`. A target is one way out of this app into an integration: `thread create target` prints every way a place can be reached, asks which, and puts that prefix on the id, so how it is reached is the prefix and no field says so. A call is one thing a target can be asked to do, declared beside it with `thread create target call`. A webhook is the way back in: `thread create webhook` asks which integration it sits under, the path something outside posts to, and the operation it invokes, and that operation names the webhook back, so neither half opens a door on its own. Nobody writes the file that answers that address, because `thread capabilities` derives it from those two rows. What a target accepts goes in `takes` — entities where it keeps things, calls where it is called, and nothing for a disk. What it needs goes in `requires`: its credentials, and the target it is reached through; where it is goes in address, and a header every call carries goes in headers. This is the one place the split between what reaches a server and what stays local is written down.';
|
|
@@ -39,7 +41,7 @@ const concept = (one) => [
|
|
|
39
41
|
' ],',
|
|
40
42
|
' },',
|
|
41
43
|
].join('\n');
|
|
42
|
-
const INTEGRATIONS_SOURCE = `import type { Threaded } from '
|
|
44
|
+
const INTEGRATIONS_SOURCE = `import type { Threaded } from '${SHAPED}'
|
|
43
45
|
|
|
44
46
|
export const INTEGRATIONS: Threaded = {
|
|
45
47
|
id: 'integrations',
|
|
@@ -62,7 +64,7 @@ export const INTEGRATIONS: Threaded = {
|
|
|
62
64
|
],
|
|
63
65
|
}
|
|
64
66
|
`;
|
|
65
|
-
const TARGETS_SOURCE = `import type { Call, Integration, Target, Webhook } from '
|
|
67
|
+
const TARGETS_SOURCE = `import type { Call, Integration, Target, Webhook } from '${NAMED}/integrations/target'
|
|
66
68
|
|
|
67
69
|
export const INTEGRATIONS: readonly Integration[] = []
|
|
68
70
|
|
|
@@ -72,7 +74,7 @@ export const CALLS: readonly Call[] = []
|
|
|
72
74
|
|
|
73
75
|
export const WEBHOOKS: readonly Webhook[] = []
|
|
74
76
|
`;
|
|
75
|
-
const SECRETS_SOURCE = `import type { Secret } from '
|
|
77
|
+
const SECRETS_SOURCE = `import type { Secret } from '${NAMED}/integrations/secret'
|
|
76
78
|
|
|
77
79
|
export const SECRETS: readonly Secret[] = []
|
|
78
80
|
`;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { Attaching } from '../core/attach.ts'
|
|
2
|
-
import {
|
|
2
|
+
import { SHAPED } from '../core/init.ts'
|
|
3
|
+
import { NAMED, named } from '../core/scope.ts'
|
|
3
4
|
|
|
4
5
|
const VERSION = 1
|
|
5
6
|
|
|
@@ -77,7 +78,7 @@ const concept = (one: (typeof CONCEPTS)[number]): string =>
|
|
|
77
78
|
' },',
|
|
78
79
|
].join('\n')
|
|
79
80
|
|
|
80
|
-
const INTEGRATIONS_SOURCE = `import type { Threaded } from '
|
|
81
|
+
const INTEGRATIONS_SOURCE = `import type { Threaded } from '${SHAPED}'
|
|
81
82
|
|
|
82
83
|
export const INTEGRATIONS: Threaded = {
|
|
83
84
|
id: 'integrations',
|
|
@@ -101,7 +102,7 @@ export const INTEGRATIONS: Threaded = {
|
|
|
101
102
|
}
|
|
102
103
|
`
|
|
103
104
|
|
|
104
|
-
const TARGETS_SOURCE = `import type { Call, Integration, Target, Webhook } from '
|
|
105
|
+
const TARGETS_SOURCE = `import type { Call, Integration, Target, Webhook } from '${NAMED}/integrations/target'
|
|
105
106
|
|
|
106
107
|
export const INTEGRATIONS: readonly Integration[] = []
|
|
107
108
|
|
|
@@ -112,7 +113,7 @@ export const CALLS: readonly Call[] = []
|
|
|
112
113
|
export const WEBHOOKS: readonly Webhook[] = []
|
|
113
114
|
`
|
|
114
115
|
|
|
115
|
-
const SECRETS_SOURCE = `import type { Secret } from '
|
|
116
|
+
const SECRETS_SOURCE = `import type { Secret } from '${NAMED}/integrations/secret'
|
|
116
117
|
|
|
117
118
|
export const SECRETS: readonly Secret[] = []
|
|
118
119
|
`
|
package/dist/integrity/attach.js
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { SHAPED } from '../core/init.js';
|
|
2
|
+
import { NAMED } from '../core/scope.js';
|
|
2
3
|
import { quoted } from '../core/rows.js';
|
|
3
4
|
const DESCRIBES = 'What this app refuses, and the checks that make each refusal actually happen. Every check has one owner, so none of them can be switched off somewhere nobody is looking.';
|
|
4
5
|
const CONSTRAINT = 'The rules the framework enforces on every app that installs it. They are read from the installed package rather than from this tree, so there is nothing here to weaken.';
|
|
5
6
|
const RULES = 'What this app refuses on top of what the framework already does. `thread create rule` asks what to ban, where it applies and what to do instead, then writes the row and the check beside it.';
|
|
6
|
-
const HOME = `node_modules/${
|
|
7
|
+
const HOME = `node_modules/${NAMED}/dist/integrity`;
|
|
7
8
|
const GATE = 'Every surface that can refuse, each naming where it lives and the command that runs it, so the gate is derived from what this system declares rather than kept as a string beside it.';
|
|
8
9
|
const COMPILER = 'The compiler run over the files this app declares and no others. A type error is the plainest kind of broken build, so it is refused here rather than found by whoever runs the app next.';
|
|
9
|
-
const INTEGRITY_SOURCE = `import type { Threaded } from '
|
|
10
|
+
const INTEGRITY_SOURCE = `import type { Threaded } from '${SHAPED}'
|
|
10
11
|
|
|
11
12
|
export const INTEGRITY: Threaded = {
|
|
12
13
|
id: 'integrity',
|
|
@@ -42,11 +43,11 @@ export const INTEGRITY: Threaded = {
|
|
|
42
43
|
],
|
|
43
44
|
}
|
|
44
45
|
`;
|
|
45
|
-
const RULES_SOURCE = `import type { Constraint } from '
|
|
46
|
+
const RULES_SOURCE = `import type { Constraint } from '${NAMED}/integrity/constraint'
|
|
46
47
|
|
|
47
48
|
export const RULES: readonly Constraint[] = []
|
|
48
49
|
`;
|
|
49
|
-
const GATE_SOURCE = `import type { Threaded } from '
|
|
50
|
+
const GATE_SOURCE = `import type { Threaded } from '${SHAPED}'
|
|
50
51
|
|
|
51
52
|
export const GATE: Threaded = {
|
|
52
53
|
id: 'gate',
|