@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.
@@ -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 '../core/thread.ts'
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,
@@ -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 '../core/thread.ts'
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,
@@ -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 { NAMED, ORGANISATION } from './scope.js';
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 \`${NAMED}/thread\` in the root, or run \`thread init\`. Without the shape there is nothing to hold anything to`,
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`,
@@ -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 { NAMED, ORGANISATION } from './scope.ts'
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 \`${NAMED}/thread\` in the root, or run \`thread init\`. Without the shape there is nothing to hold anything to`,
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
+ }
@@ -1,5 +1,6 @@
1
1
  export declare const TREE = "_threads";
2
2
  export declare const ROOT = "thread.ts";
3
+ export declare const SHAPED = "@prestack/thread/core/thread";
3
4
  export declare class AlreadyThreadedError extends Error {
4
5
  readonly at: string;
5
6
  constructor(at: string);
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
- "import type { Threaded } from './thread.ts'",
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
- "import type { Threaded } from './thread.ts'",
129
+ `import type { Threaded } from '${SHAPED}'`,
127
130
  '',
128
131
  'export const THREAD: Threaded = {',
129
132
  " id: 'thread',",
@@ -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;
@@ -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('/'));
@@ -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
- "import type { Threaded } from '../core/thread.ts'",
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
- "import type { Threaded } from '../core/thread.ts'",
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 '../core/thread.ts'
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 './target.ts'
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 './secret.ts'
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 { named } from '../core/scope.ts'
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 '../core/thread.ts'
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 './target.ts'
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 './secret.ts'
116
+ const SECRETS_SOURCE = `import type { Secret } from '${NAMED}/integrations/secret'
116
117
 
117
118
  export const SECRETS: readonly Secret[] = []
118
119
  `
@@ -1,12 +1,13 @@
1
- import { named } from '../core/scope.js';
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/${named('integrity')}/dist`;
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 '../core/thread.ts'
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 './constraint.ts'
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 '../core/thread.ts'
50
+ const GATE_SOURCE = `import type { Threaded } from '${SHAPED}'
50
51
 
51
52
  export const GATE: Threaded = {
52
53
  id: 'gate',