accrete-kit 0.0.0-stage → 0.1.1
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 +48 -0
- package/LICENSE +21 -0
- package/README.md +148 -2
- package/bin/accrete-kit.mjs +4 -0
- package/dist/kernel/darwin-arm64/LICENSE +21 -0
- package/dist/kernel/darwin-arm64/THIRD-PARTY-NOTICES +5780 -0
- package/dist/kernel/darwin-arm64/accrete +0 -0
- package/dist/kernel/darwin-arm64/licenses/RUST-COPYRIGHT-library.html +7038 -0
- package/dist/kernel/darwin-arm64/licenses/musl-COPYRIGHT +193 -0
- package/dist/kernel/darwin-arm64/licenses/rust/Apache-2.0.txt +73 -0
- package/dist/kernel/darwin-arm64/licenses/rust/BSD-2-Clause.txt +9 -0
- package/dist/kernel/darwin-arm64/licenses/rust/CC-BY-SA-4.0.txt +427 -0
- package/dist/kernel/darwin-arm64/licenses/rust/GCC-exception-3.1.txt +30 -0
- package/dist/kernel/darwin-arm64/licenses/rust/GPL-2.0-only.txt +133 -0
- package/dist/kernel/darwin-arm64/licenses/rust/GPL-3.0-or-later.txt +202 -0
- package/dist/kernel/darwin-arm64/licenses/rust/ISC.txt +7 -0
- package/dist/kernel/darwin-arm64/licenses/rust/LLVM-exception.txt +15 -0
- package/dist/kernel/darwin-arm64/licenses/rust/MIT.txt +9 -0
- package/dist/kernel/darwin-arm64/licenses/rust/NCSA.txt +29 -0
- package/dist/kernel/darwin-arm64/licenses/rust/OFL-1.1.txt +43 -0
- package/dist/kernel/darwin-arm64/licenses/rust/Unicode-3.0.txt +39 -0
- package/dist/kernel/darwin-x64/LICENSE +21 -0
- package/dist/kernel/darwin-x64/THIRD-PARTY-NOTICES +5780 -0
- package/dist/kernel/darwin-x64/accrete +0 -0
- package/dist/kernel/darwin-x64/licenses/RUST-COPYRIGHT-library.html +7038 -0
- package/dist/kernel/darwin-x64/licenses/musl-COPYRIGHT +193 -0
- package/dist/kernel/darwin-x64/licenses/rust/Apache-2.0.txt +73 -0
- package/dist/kernel/darwin-x64/licenses/rust/BSD-2-Clause.txt +9 -0
- package/dist/kernel/darwin-x64/licenses/rust/CC-BY-SA-4.0.txt +427 -0
- package/dist/kernel/darwin-x64/licenses/rust/GCC-exception-3.1.txt +30 -0
- package/dist/kernel/darwin-x64/licenses/rust/GPL-2.0-only.txt +133 -0
- package/dist/kernel/darwin-x64/licenses/rust/GPL-3.0-or-later.txt +202 -0
- package/dist/kernel/darwin-x64/licenses/rust/ISC.txt +7 -0
- package/dist/kernel/darwin-x64/licenses/rust/LLVM-exception.txt +15 -0
- package/dist/kernel/darwin-x64/licenses/rust/MIT.txt +9 -0
- package/dist/kernel/darwin-x64/licenses/rust/NCSA.txt +29 -0
- package/dist/kernel/darwin-x64/licenses/rust/OFL-1.1.txt +43 -0
- package/dist/kernel/darwin-x64/licenses/rust/Unicode-3.0.txt +39 -0
- package/dist/kernel/kernel.json +17 -0
- package/dist/kernel/linux-arm64/LICENSE +21 -0
- package/dist/kernel/linux-arm64/THIRD-PARTY-NOTICES +5780 -0
- package/dist/kernel/linux-arm64/accrete +0 -0
- package/dist/kernel/linux-arm64/licenses/RUST-COPYRIGHT-library.html +7038 -0
- package/dist/kernel/linux-arm64/licenses/musl-COPYRIGHT +193 -0
- package/dist/kernel/linux-arm64/licenses/rust/Apache-2.0.txt +73 -0
- package/dist/kernel/linux-arm64/licenses/rust/BSD-2-Clause.txt +9 -0
- package/dist/kernel/linux-arm64/licenses/rust/CC-BY-SA-4.0.txt +427 -0
- package/dist/kernel/linux-arm64/licenses/rust/GCC-exception-3.1.txt +30 -0
- package/dist/kernel/linux-arm64/licenses/rust/GPL-2.0-only.txt +133 -0
- package/dist/kernel/linux-arm64/licenses/rust/GPL-3.0-or-later.txt +202 -0
- package/dist/kernel/linux-arm64/licenses/rust/ISC.txt +7 -0
- package/dist/kernel/linux-arm64/licenses/rust/LLVM-exception.txt +15 -0
- package/dist/kernel/linux-arm64/licenses/rust/MIT.txt +9 -0
- package/dist/kernel/linux-arm64/licenses/rust/NCSA.txt +29 -0
- package/dist/kernel/linux-arm64/licenses/rust/OFL-1.1.txt +43 -0
- package/dist/kernel/linux-arm64/licenses/rust/Unicode-3.0.txt +39 -0
- package/dist/kernel/linux-x64/LICENSE +21 -0
- package/dist/kernel/linux-x64/THIRD-PARTY-NOTICES +5780 -0
- package/dist/kernel/linux-x64/accrete +0 -0
- package/dist/kernel/linux-x64/licenses/RUST-COPYRIGHT-library.html +7038 -0
- package/dist/kernel/linux-x64/licenses/musl-COPYRIGHT +193 -0
- package/dist/kernel/linux-x64/licenses/rust/Apache-2.0.txt +73 -0
- package/dist/kernel/linux-x64/licenses/rust/BSD-2-Clause.txt +9 -0
- package/dist/kernel/linux-x64/licenses/rust/CC-BY-SA-4.0.txt +427 -0
- package/dist/kernel/linux-x64/licenses/rust/GCC-exception-3.1.txt +30 -0
- package/dist/kernel/linux-x64/licenses/rust/GPL-2.0-only.txt +133 -0
- package/dist/kernel/linux-x64/licenses/rust/GPL-3.0-or-later.txt +202 -0
- package/dist/kernel/linux-x64/licenses/rust/ISC.txt +7 -0
- package/dist/kernel/linux-x64/licenses/rust/LLVM-exception.txt +15 -0
- package/dist/kernel/linux-x64/licenses/rust/MIT.txt +9 -0
- package/dist/kernel/linux-x64/licenses/rust/NCSA.txt +29 -0
- package/dist/kernel/linux-x64/licenses/rust/OFL-1.1.txt +43 -0
- package/dist/kernel/linux-x64/licenses/rust/Unicode-3.0.txt +39 -0
- package/dist/scaffold/assets/contract/cli-v1.json +296 -0
- package/dist/scaffold/assets/contract/example-inputs.json +38 -0
- package/dist/scaffold/assets/contract/input-drafts.schema.json +654 -0
- package/dist/scaffold/assets/generated/cli-commands.md +288 -0
- package/dist/scaffold/assets/generated/release.json +11 -0
- package/dist/scaffold/assets/skills/accrete-data/SKILL.md +73 -0
- package/dist/scaffold/assets/skills/accrete-data/references/commands.md +193 -0
- package/dist/scaffold/assets/skills/accrete-data/references/objects-and-native-work.md +123 -0
- package/dist/scaffold/assets/skills/accrete-data/references/remote-and-retention.md +98 -0
- package/dist/scaffold/assets/skills/accrete-memory/SKILL.md +61 -0
- package/dist/scaffold/assets/skills/accrete-memory/references/commands.md +124 -0
- package/dist/scaffold/assets/skills/accrete-memory/references/handovers-and-remote-work.md +78 -0
- package/dist/scaffold/assets/skills/accrete-memory/references/writing-and-continuity.md +101 -0
- package/dist/scaffold/assets/skills/accrete-models/SKILL.md +46 -0
- package/dist/scaffold/assets/skills/accrete-models/references/cli.md +117 -0
- package/dist/scaffold/assets/skills/accrete-models/references/objects.md +67 -0
- package/dist/scaffold/assets/skills/accrete-models/references/working-practice.md +48 -0
- package/dist/scaffold/assets/skills/accrete-results/SKILL.md +69 -0
- package/dist/scaffold/assets/skills/accrete-results/references/activities-and-relations.md +116 -0
- package/dist/scaffold/assets/skills/accrete-results/references/commands.md +191 -0
- package/dist/scaffold/assets/skills/accrete-results/references/scientific-practice.md +89 -0
- package/dist/scaffold/assets/skills/accrete-tools/SKILL.md +51 -0
- package/dist/scaffold/assets/skills/accrete-tools/references/cli.md +87 -0
- package/dist/scaffold/assets/skills/accrete-tools/references/objects.md +66 -0
- package/dist/scaffold/assets/skills/accrete-tools/references/working-practice.md +51 -0
- package/dist/scaffold/assets/template/.accrete/PROJECT.md +31 -0
- package/dist/scaffold/assets/template/.accrete/config-examples/local-compute.toml.example +27 -0
- package/dist/scaffold/assets/template/.accrete/config-examples/local-workstation.toml.example +21 -0
- package/dist/scaffold/assets/template/.accrete/config-examples/project.toml.example +27 -0
- package/dist/scaffold/assets/template/.accrete/docs/cli-contract.md +393 -0
- package/dist/scaffold/assets/template/.accrete/docs/configuration.md +117 -0
- package/dist/scaffold/assets/template/.accrete/docs/coordinator.md +102 -0
- package/dist/scaffold/assets/template/.accrete/docs/long-term-work.md +108 -0
- package/dist/scaffold/assets/template/.accrete/docs/overview.md +118 -0
- package/dist/scaffold/assets/template/.accrete/docs/remote-work.md +125 -0
- package/dist/scaffold/assets/template/.accrete/docs/subagents.md +78 -0
- package/dist/scaffold/assets/template/.accrete/examples/README.md +78 -0
- package/dist/scaffold/assets/template/.accrete/examples/data-register.json +19 -0
- package/dist/scaffold/assets/template/.accrete/examples/iteration-close.json +5 -0
- package/dist/scaffold/assets/template/.accrete/examples/iteration-open.json +8 -0
- package/dist/scaffold/assets/template/.accrete/examples/memory-entry.md +10 -0
- package/dist/scaffold/assets/template/.accrete/examples/model-register.json +7 -0
- package/dist/scaffold/assets/template/.accrete/examples/observation.md +9 -0
- package/dist/scaffold/assets/template/.accrete/examples/result-amend.json +5 -0
- package/dist/scaffold/assets/template/.accrete/examples/result-record.json +11 -0
- package/dist/scaffold/assets/template/.accrete/examples/tool-admit.json +8 -0
- package/dist/scaffold/assets/template/.accrete/examples/tool-retire.json +3 -0
- package/dist/scaffold/assets/template/.accrete/examples/tool-update.json +3 -0
- package/dist/scaffold/assets/template/.gitattributes +17 -0
- package/dist/scaffold/assets/template/AGENTS.md +23 -0
- package/dist/scaffold/assets/template/CLAUDE.md +5 -0
- package/dist/scaffold/assets/template/_gitignore +12 -0
- package/dist/scaffold/config/defaults.toml +20 -0
- package/dist/scaffold/manifest.json +622 -0
- package/lib/cli.mjs +188 -0
- package/lib/fsutil.mjs +135 -0
- package/lib/install.mjs +439 -0
- package/lib/kernel.mjs +76 -0
- package/lib/scaffold.mjs +129 -0
- package/lib/workspace.mjs +278 -0
- package/package.json +18 -4
package/lib/cli.mjs
ADDED
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
// Arguments and what the person or agent reads.
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { parseArgs } from 'node:util';
|
|
5
|
+
import { AccreteError } from './fsutil.mjs';
|
|
6
|
+
import { BINARY } from './kernel.mjs';
|
|
7
|
+
import { MARKER, loadScaffold } from './scaffold.mjs';
|
|
8
|
+
import { createWorkspace, findWorkspace, installWorkspace, upgradeWorkspace } from './workspace.mjs';
|
|
9
|
+
|
|
10
|
+
const USAGE = `Create or upgrade an ACCRETE Workspace, and install its Kernel.
|
|
11
|
+
|
|
12
|
+
npx accrete-kit@latest create DIRECTORY [OPTIONS]
|
|
13
|
+
npx accrete-kit@latest create . Add to this directory or repository
|
|
14
|
+
npx accrete-kit@latest upgrade [OPTIONS]
|
|
15
|
+
npx accrete-kit@<version> install [OPTIONS] Restore this host's Kernel after a clone
|
|
16
|
+
|
|
17
|
+
Create options:
|
|
18
|
+
--name NAME --id ID --timezone IANA_ZONE
|
|
19
|
+
--agents codex,claude|codex|claude|none
|
|
20
|
+
--no-git --no-install --json
|
|
21
|
+
|
|
22
|
+
Upgrade options:
|
|
23
|
+
--project PATH Otherwise the nearest ancestor ${MARKER}
|
|
24
|
+
--dry-run Report the decisions and write nothing
|
|
25
|
+
--agents codex,claude|codex|claude|none
|
|
26
|
+
--accept PATH Record a resolved candidate, relative to the Workspace
|
|
27
|
+
--no-install --json
|
|
28
|
+
|
|
29
|
+
Install options:
|
|
30
|
+
--project PATH --json
|
|
31
|
+
|
|
32
|
+
--no-install skips installing the Kernel binary. Owner edits are preserved: a conflict
|
|
33
|
+
leaves the file alone and writes a candidate under .accrete/template-upgrades/. The Kernel
|
|
34
|
+
runs as ${BINARY}; accrete-kit does not forward commands to it.`;
|
|
35
|
+
|
|
36
|
+
const OPTIONS = {
|
|
37
|
+
name: { type: 'string' },
|
|
38
|
+
id: { type: 'string' },
|
|
39
|
+
timezone: { type: 'string' },
|
|
40
|
+
agents: { type: 'string' },
|
|
41
|
+
'no-git': { type: 'boolean' },
|
|
42
|
+
'no-install': { type: 'boolean' },
|
|
43
|
+
project: { type: 'string' },
|
|
44
|
+
'dry-run': { type: 'boolean' },
|
|
45
|
+
accept: { type: 'string' },
|
|
46
|
+
json: { type: 'boolean' },
|
|
47
|
+
help: { type: 'boolean', short: 'h' },
|
|
48
|
+
version: { type: 'boolean', short: 'v' }
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
function refuse(values, names, context) {
|
|
52
|
+
for (const name of names) {
|
|
53
|
+
if (values[name] !== undefined) throw new AccreteError('invalid', `--${name} does not apply ${context}`);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function nextSteps(receipt, cwd) {
|
|
58
|
+
const steps = [];
|
|
59
|
+
const fresh = receipt.action === 'create' && !receipt.skipped;
|
|
60
|
+
if (fresh) {
|
|
61
|
+
const relative = path.relative(cwd, receipt.project_root);
|
|
62
|
+
if (relative) steps.push(`cd ${relative.startsWith('..') ? receipt.project_root : relative}`);
|
|
63
|
+
}
|
|
64
|
+
if (receipt.kernel?.action === 'skipped' && !receipt.dry_run) steps.push(`npx accrete-kit@${receipt.template_version} install`);
|
|
65
|
+
if (fresh) {
|
|
66
|
+
if (receipt.ok) steps.push(`${BINARY} --version`);
|
|
67
|
+
steps.push('Start your agent at the Workspace root; AGENTS.md is its entry point');
|
|
68
|
+
}
|
|
69
|
+
return steps;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function printHuman(receipt) {
|
|
73
|
+
const out = line => process.stdout.write(`${line}\n`);
|
|
74
|
+
const where = receipt.project_root;
|
|
75
|
+
if (receipt.skipped) out(receipt.message);
|
|
76
|
+
else if (receipt.action === 'accept') out(`${receipt.dry_run ? 'Would accept' : 'Accepted'} ${receipt.accepted.destination} in ${where}`);
|
|
77
|
+
else if (receipt.action === 'install') out(`Kernel for ${receipt.kernel.platform} in ${where}`);
|
|
78
|
+
else if (receipt.dry_run) out(`Would apply template ${receipt.template_version} to ${where}`);
|
|
79
|
+
else {
|
|
80
|
+
const verb = receipt.action === 'upgrade' ? 'Upgraded' : receipt.created ? 'Created' : 'Updated';
|
|
81
|
+
out(`${verb} ACCRETE Workspace ${where} (template ${receipt.template_version})`);
|
|
82
|
+
}
|
|
83
|
+
if (receipt.git_initialized) out('Initialized a Git repository; nothing is committed.');
|
|
84
|
+
if (receipt.kernel) out(`Kernel: accrete ${receipt.kernel.version} at ${receipt.kernel.path} (${receipt.kernel.action})`);
|
|
85
|
+
if (receipt.files) {
|
|
86
|
+
const counts = new Map();
|
|
87
|
+
for (const { action } of receipt.files) counts.set(action, (counts.get(action) ?? 0) + 1);
|
|
88
|
+
if (counts.size > 0) out(` ${[...counts].map(([action, count]) => `${action}: ${count}`).join(', ')}`);
|
|
89
|
+
for (const { destination, candidate, next } of receipt.files) {
|
|
90
|
+
if (candidate) out(` pending ${destination}\n candidate ${candidate}\n ${next}`);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
for (const destination of receipt.detached ?? []) out(` detached ${destination} (edited; no longer managed)`);
|
|
94
|
+
for (const destination of receipt.locally_deleted ?? []) out(` kept deleted ${destination}`);
|
|
95
|
+
for (const { destination, error, retry } of receipt.failed) {
|
|
96
|
+
out(` failed ${destination}: ${error}`);
|
|
97
|
+
if (retry) out(` retry: ${retry}`);
|
|
98
|
+
}
|
|
99
|
+
if (receipt.next_steps.length > 0) {
|
|
100
|
+
out('\nNext steps:');
|
|
101
|
+
for (const step of receipt.next_steps) out(` ${step}`);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function selectWorkspace(values, directories, cwd, command) {
|
|
106
|
+
if (directories.length > 0) throw new AccreteError('invalid', `${command} takes no directory; use --project ${directories[0]}`);
|
|
107
|
+
const named = values.project === undefined ? null : path.resolve(cwd, values.project);
|
|
108
|
+
const root = named === null ? findWorkspace(cwd) : fs.existsSync(named) ? fs.realpathSync(named) : null;
|
|
109
|
+
if (!root || !fs.existsSync(path.join(root, MARKER))) {
|
|
110
|
+
throw new AccreteError('not_found', `No ACCRETE Workspace (${MARKER}) at ${values.project ?? `or above ${cwd}`}; create one with npx accrete-kit@latest create`);
|
|
111
|
+
}
|
|
112
|
+
return root;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function install(values, directories, scaffold, cwd) {
|
|
116
|
+
refuse(values, ['name', 'id', 'timezone', 'no-git', 'no-install', 'dry-run', 'agents', 'accept'], 'to install');
|
|
117
|
+
return installWorkspace(selectWorkspace(values, directories, cwd, 'install'), scaffold);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function upgrade(values, directories, scaffold, cwd) {
|
|
121
|
+
refuse(values, ['name', 'id', 'timezone', 'no-git'], 'to upgrade');
|
|
122
|
+
if (values.accept !== undefined && values.agents !== undefined) throw new AccreteError('invalid', '--accept and --agents are separate operations');
|
|
123
|
+
const root = selectWorkspace(values, directories, cwd, 'upgrade');
|
|
124
|
+
return upgradeWorkspace(root, {
|
|
125
|
+
agents: values.agents,
|
|
126
|
+
accept: values.accept,
|
|
127
|
+
dryRun: values['dry-run'],
|
|
128
|
+
install: !values['no-install']
|
|
129
|
+
}, scaffold);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function create(values, directories, scaffold, cwd) {
|
|
133
|
+
refuse(values, ['project', 'dry-run', 'accept'], 'to create');
|
|
134
|
+
if (directories.length !== 1) throw new AccreteError('invalid', 'create takes one directory, or . for this one');
|
|
135
|
+
return createWorkspace(path.resolve(cwd, directories[0]), {
|
|
136
|
+
name: values.name,
|
|
137
|
+
id: values.id,
|
|
138
|
+
timezone: values.timezone,
|
|
139
|
+
agents: values.agents,
|
|
140
|
+
git: !values['no-git'],
|
|
141
|
+
install: !values['no-install']
|
|
142
|
+
}, scaffold);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const COMMANDS = { create, upgrade, install };
|
|
146
|
+
|
|
147
|
+
export function main(argv, cwd = process.cwd()) {
|
|
148
|
+
const jsonRequested = argv.includes('--json');
|
|
149
|
+
try {
|
|
150
|
+
let parsed;
|
|
151
|
+
try {
|
|
152
|
+
parsed = parseArgs({ args: argv, options: OPTIONS, allowPositionals: true });
|
|
153
|
+
} catch (error) {
|
|
154
|
+
throw new AccreteError('invalid', error.message);
|
|
155
|
+
}
|
|
156
|
+
const { values, positionals } = parsed;
|
|
157
|
+
if (values.help) {
|
|
158
|
+
process.stdout.write(`${USAGE}\n`);
|
|
159
|
+
return 0;
|
|
160
|
+
}
|
|
161
|
+
const scaffold = loadScaffold();
|
|
162
|
+
if (values.version) {
|
|
163
|
+
process.stdout.write(`accrete-kit ${scaffold.template_version} (carries Kernel ${scaffold.kernel_version})\n`);
|
|
164
|
+
return 0;
|
|
165
|
+
}
|
|
166
|
+
const [command, ...directories] = positionals;
|
|
167
|
+
if (!Object.hasOwn(COMMANDS, command ?? '')) {
|
|
168
|
+
throw new AccreteError('invalid', command === undefined ? 'Name a subcommand: create, upgrade or install' : `Unknown subcommand ${command}; use create, upgrade or install`);
|
|
169
|
+
}
|
|
170
|
+
const receipt = COMMANDS[command](values, directories, scaffold, cwd);
|
|
171
|
+
receipt.next_steps = nextSteps(receipt, cwd);
|
|
172
|
+
if (receipt.warning) process.stderr.write(`warning: ${receipt.warning}\n`);
|
|
173
|
+
for (const warning of receipt.warnings ?? []) process.stderr.write(`warning: ${warning}\n`);
|
|
174
|
+
if (receipt.cleanup_warning) process.stderr.write(`warning: cleanup: ${receipt.cleanup_warning}\n`);
|
|
175
|
+
if (values.json) process.stdout.write(`${JSON.stringify(receipt)}\n`);
|
|
176
|
+
else printHuman(receipt);
|
|
177
|
+
return receipt.ok ? 0 : 7;
|
|
178
|
+
} catch (caught) {
|
|
179
|
+
const error = caught instanceof AccreteError ? caught : new AccreteError('storage', caught.message);
|
|
180
|
+
if (jsonRequested) process.stderr.write(`${JSON.stringify(error)}\n`);
|
|
181
|
+
else {
|
|
182
|
+
process.stderr.write(`${error.kind}: ${error.message}\n`);
|
|
183
|
+
if (error.message.includes('subcommand')) process.stderr.write(`\n${USAGE}\n`);
|
|
184
|
+
if (error.details) process.stderr.write(`${JSON.stringify(error.details, null, 2)}\n`);
|
|
185
|
+
}
|
|
186
|
+
return error.exitCode;
|
|
187
|
+
}
|
|
188
|
+
}
|
package/lib/fsutil.mjs
ADDED
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
// Errors and the file operations every other module goes through.
|
|
2
|
+
import { createHash } from 'node:crypto';
|
|
3
|
+
import fs from 'node:fs';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
|
|
6
|
+
// The same kinds and exit codes as the accrete Kernel.
|
|
7
|
+
const EXIT_CODES = {
|
|
8
|
+
invalid: 2, unsupported_format: 2, config: 3, not_found: 3, conflict: 4,
|
|
9
|
+
dependency: 5, storage: 6, external: 7, incomplete: 7
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export class AccreteError extends Error {
|
|
13
|
+
constructor(kind, message, details = null) {
|
|
14
|
+
super(message);
|
|
15
|
+
this.kind = kind;
|
|
16
|
+
this.details = details;
|
|
17
|
+
this.exitCode = EXIT_CODES[kind] ?? 6;
|
|
18
|
+
}
|
|
19
|
+
toJSON() {
|
|
20
|
+
return {
|
|
21
|
+
ok: false,
|
|
22
|
+
error: { kind: this.kind, message: this.message, details: this.details },
|
|
23
|
+
exit_code: this.exitCode
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export const sha256 = bytes => createHash('sha256').update(bytes).digest('hex');
|
|
29
|
+
|
|
30
|
+
/** A forward-slash relative path with normal components only. */
|
|
31
|
+
export function checkRelative(text) {
|
|
32
|
+
const portable = typeof text === 'string' && text.length > 0
|
|
33
|
+
&& !/[\\:\u0000-\u001f]/.test(text)
|
|
34
|
+
&& !text.startsWith('/')
|
|
35
|
+
&& !text.split('/').some(part => part === '' || part === '.' || part === '..');
|
|
36
|
+
if (!portable) throw new AccreteError('invalid', `Paths must be portable relative paths without traversal: ${text}`);
|
|
37
|
+
return text;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const lstat = target => {
|
|
41
|
+
try {
|
|
42
|
+
return fs.lstatSync(target);
|
|
43
|
+
} catch (error) {
|
|
44
|
+
if (error.code === 'ENOENT' || error.code === 'ENOTDIR') return null;
|
|
45
|
+
throw error;
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
export function refuseSymlinks(root, relative) {
|
|
50
|
+
let current = root;
|
|
51
|
+
for (const part of relative.split('/')) {
|
|
52
|
+
current = path.join(current, part);
|
|
53
|
+
if (lstat(current)?.isSymbolicLink()) {
|
|
54
|
+
throw new AccreteError('conflict', `Path traverses a symbolic link: ${current}`);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* What is at a destination, without following links: `missing`, a regular
|
|
61
|
+
* `file` with its bytes, or `other` for a directory, symbolic link or special
|
|
62
|
+
* file at or above it.
|
|
63
|
+
*/
|
|
64
|
+
export function readCurrent(root, destination) {
|
|
65
|
+
const parts = checkRelative(destination).split('/');
|
|
66
|
+
let current = root;
|
|
67
|
+
for (const [index, part] of parts.entries()) {
|
|
68
|
+
current = path.join(current, part);
|
|
69
|
+
const stat = lstat(current);
|
|
70
|
+
if (!stat) return { kind: 'missing' };
|
|
71
|
+
if (index === parts.length - 1) {
|
|
72
|
+
return stat.isFile() ? { kind: 'file', bytes: fs.readFileSync(current) } : { kind: 'other' };
|
|
73
|
+
}
|
|
74
|
+
if (!stat.isDirectory()) return { kind: 'other' };
|
|
75
|
+
}
|
|
76
|
+
return { kind: 'other' };
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function syncDirectory(directory) {
|
|
80
|
+
let fd;
|
|
81
|
+
try {
|
|
82
|
+
fd = fs.openSync(directory, 'r');
|
|
83
|
+
fs.fsyncSync(fd);
|
|
84
|
+
} catch (error) {
|
|
85
|
+
// Some platforms cannot open or synchronize a directory.
|
|
86
|
+
if (!['EISDIR', 'EPERM', 'EINVAL', 'ENOTSUP'].includes(error.code)) throw error;
|
|
87
|
+
} finally {
|
|
88
|
+
if (fd !== undefined) fs.closeSync(fd);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Temp sibling, fsync, rename, fsync the directory. The temp name is fixed, so
|
|
94
|
+
* the next write of this path replaces one a killed run left behind.
|
|
95
|
+
*/
|
|
96
|
+
export function atomicWrite(target, bytes, { mode: requested } = {}) {
|
|
97
|
+
const directory = path.dirname(target);
|
|
98
|
+
const firstCreated = fs.mkdirSync(directory, { recursive: true });
|
|
99
|
+
const temporary = path.join(directory, `.${path.basename(target)}.accrete-tmp`);
|
|
100
|
+
const mode = requested ?? lstat(target)?.mode;
|
|
101
|
+
fs.rmSync(temporary, { force: true });
|
|
102
|
+
try {
|
|
103
|
+
const fd = fs.openSync(temporary, 'wx', 0o644);
|
|
104
|
+
try {
|
|
105
|
+
fs.writeFileSync(fd, bytes);
|
|
106
|
+
if (mode !== undefined) fs.fchmodSync(fd, mode & 0o7777);
|
|
107
|
+
fs.fsyncSync(fd);
|
|
108
|
+
} finally {
|
|
109
|
+
fs.closeSync(fd);
|
|
110
|
+
}
|
|
111
|
+
fs.renameSync(temporary, target);
|
|
112
|
+
} catch (error) {
|
|
113
|
+
fs.rmSync(temporary, { force: true });
|
|
114
|
+
throw error;
|
|
115
|
+
}
|
|
116
|
+
syncDirectory(directory);
|
|
117
|
+
if (firstCreated) {
|
|
118
|
+
for (let up = directory; up !== path.dirname(firstCreated); up = path.dirname(up)) syncDirectory(path.dirname(up));
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export function removeWithEmptyParents(root, destination) {
|
|
123
|
+
const target = path.join(root, destination);
|
|
124
|
+
fs.rmSync(target);
|
|
125
|
+
let directory = path.dirname(target);
|
|
126
|
+
while (directory !== root) {
|
|
127
|
+
try {
|
|
128
|
+
fs.rmdirSync(directory);
|
|
129
|
+
} catch {
|
|
130
|
+
break;
|
|
131
|
+
}
|
|
132
|
+
directory = path.dirname(directory);
|
|
133
|
+
}
|
|
134
|
+
syncDirectory(directory);
|
|
135
|
+
}
|