@yunhozz/arc 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/MANIFEST.sha256 +96 -0
- package/README.md +265 -2
- package/bin/arc.mjs +3 -0
- package/claude/agents/arc-coder.md +67 -0
- package/claude/agents/arc-planner.md +74 -0
- package/claude/agents/arc-qa.md +68 -0
- package/claude/agents/arc-reviewer.md +65 -0
- package/claude/agents/arc-scouter.md +63 -0
- package/claude/skill/SKILL.md +370 -0
- package/claude/skill/hooks/git-policy.json +208 -0
- package/claude/skill/hooks/role-git-guard.mjs +98 -0
- package/claude/skill/models.toml +64 -0
- package/claude/skill/references/commit.md +77 -0
- package/claude/skill/references/git-recovery.md +46 -0
- package/claude/skill/references/handoff-prep.md +102 -0
- package/claude/skill/references/handoff.md +100 -0
- package/claude/skill/references/limits.md +42 -0
- package/claude/skill/references/planning.md +250 -0
- package/claude/skill/references/superpowers/LICENSE +21 -0
- package/claude/skill/references/superpowers/brainstorming/SKILL.md +225 -0
- package/claude/skill/references/superpowers/brainstorming/builder-check-prompt.md +23 -0
- package/claude/skill/references/superpowers/brainstorming/scripts/frame-template.html +213 -0
- package/claude/skill/references/superpowers/brainstorming/scripts/helper.js +167 -0
- package/claude/skill/references/superpowers/brainstorming/scripts/server.cjs +723 -0
- package/claude/skill/references/superpowers/brainstorming/scripts/start-server.sh +209 -0
- package/claude/skill/references/superpowers/brainstorming/scripts/stop-server.sh +120 -0
- package/claude/skill/references/superpowers/brainstorming/visual-companion.md +299 -0
- package/claude/skill/references/superpowers/provenance.json +26 -0
- package/claude/skill/references/superpowers/test-driven-development/SKILL.md +330 -0
- package/claude/skill/references/superpowers/test-driven-development/writing-good-tests.md +198 -0
- package/claude/skill/references/superpowers/writing-plans/SKILL.md +204 -0
- package/claude/skill/references/verification.md +125 -0
- package/claude/skill/references/visual-companion.md +16 -0
- package/claude/skill/scripts/apply-models.py +31 -0
- package/claude/skill/scripts/model_config.py +211 -0
- package/codex/agents/arc-coder.toml +37 -0
- package/codex/agents/arc-planner.toml +38 -0
- package/codex/agents/arc-qa.toml +24 -0
- package/codex/agents/arc-reviewer.toml +25 -0
- package/codex/agents/arc-scouter.toml +37 -0
- package/codex/skill/SKILL.md +356 -0
- package/codex/skill/models.toml +64 -0
- package/codex/skill/references/commit.md +100 -0
- package/codex/skill/references/git-recovery.md +64 -0
- package/codex/skill/references/handoff-prep.md +153 -0
- package/codex/skill/references/handoff-worktree.md +93 -0
- package/codex/skill/references/handoff.md +181 -0
- package/codex/skill/references/limits.md +43 -0
- package/codex/skill/references/planning.md +287 -0
- package/codex/skill/references/superpowers/LICENSE +21 -0
- package/codex/skill/references/superpowers/brainstorming/SKILL.md +225 -0
- package/codex/skill/references/superpowers/brainstorming/builder-check-prompt.md +23 -0
- package/codex/skill/references/superpowers/brainstorming/scripts/frame-template.html +213 -0
- package/codex/skill/references/superpowers/brainstorming/scripts/helper.js +167 -0
- package/codex/skill/references/superpowers/brainstorming/scripts/server.cjs +723 -0
- package/codex/skill/references/superpowers/brainstorming/scripts/start-server.sh +209 -0
- package/codex/skill/references/superpowers/brainstorming/scripts/stop-server.sh +120 -0
- package/codex/skill/references/superpowers/brainstorming/visual-companion.md +299 -0
- package/codex/skill/references/superpowers/provenance.json +26 -0
- package/codex/skill/references/superpowers/test-driven-development/SKILL.md +330 -0
- package/codex/skill/references/superpowers/test-driven-development/writing-good-tests.md +198 -0
- package/codex/skill/references/superpowers/writing-plans/SKILL.md +204 -0
- package/codex/skill/references/verification.md +163 -0
- package/codex/skill/references/visual-companion.md +23 -0
- package/codex/skill/scripts/apply-models.py +207 -0
- package/codex/skill/scripts/model_config.py +362 -0
- package/install.command +5 -0
- package/lib/cli/choices.mjs +47 -0
- package/lib/cli/completion.mjs +53 -0
- package/lib/cli/index.mjs +55 -0
- package/lib/cli/preset.mjs +29 -0
- package/lib/cli/progress.mjs +10 -0
- package/lib/cli/root.mjs +20 -0
- package/lib/cli/uninstall.mjs +17 -0
- package/lib/installer/acquisition.mjs +77 -0
- package/lib/installer/global-config.mjs +43 -0
- package/lib/installer/install.mjs +28 -0
- package/lib/installer/language.mjs +13 -0
- package/lib/installer/limits.mjs +21 -0
- package/lib/installer/manifest.mjs +37 -0
- package/lib/installer/package-references.mjs +32 -0
- package/lib/installer/plan.mjs +62 -0
- package/lib/installer/preset.mjs +26 -0
- package/lib/installer/runtime-config.mjs +85 -0
- package/lib/installer/shell-completion.mjs +29 -0
- package/lib/installer/stage-cleanup.mjs +40 -0
- package/lib/installer/superpowers.mjs +113 -0
- package/lib/installer/targets.mjs +55 -0
- package/lib/installer/transaction.mjs +91 -0
- package/lib/installer/uninstall.mjs +32 -0
- package/lib/limits.mjs +13 -0
- package/lib/models/config.mjs +58 -0
- package/lib/models/index.mjs +2 -0
- package/lib/models/profiles.mjs +111 -0
- package/lib/policy/claude-hook.mjs +40 -0
- package/lib/policy/render.mjs +74 -0
- package/lib/policy/role-policy.json +577 -0
- package/lib/safe-fs/index.mjs +67 -0
- package/lib/safe-fs/native.mjs +54 -0
- package/native/binding.c +157 -0
- package/native/binding.gyp +1 -0
- package/package.json +43 -4
- package/prebuilds/darwin-arm64/arc_safe_fs.node +0 -0
- package/prebuilds/darwin-x64/arc_safe_fs.node +0 -0
- package/prebuilds/index.json +64 -0
- package/prebuilds/linux-arm64-gnu/arc_safe_fs.node +0 -0
- package/prebuilds/linux-x64-gnu/arc_safe_fs.node +0 -0
- package/scripts/build-native.mjs +36 -0
- package/scripts/check-installer-parity.mjs +59 -0
- package/scripts/update-manifest.mjs +28 -0
- package/scripts/update-role-policy.mjs +15 -0
- package/scripts/update-superpowers.mjs +4 -0
- package/scripts/vendor-toml.mjs +40 -0
- package/vendor/toml/LICENSE +24 -0
- package/vendor/toml/dist/date.js +156 -0
- package/vendor/toml/dist/error.js +67 -0
- package/vendor/toml/dist/extract.js +293 -0
- package/vendor/toml/dist/index.js +34 -0
- package/vendor/toml/dist/parse.js +164 -0
- package/vendor/toml/dist/primitive.js +151 -0
- package/vendor/toml/dist/stringify.js +219 -0
- package/vendor/toml/dist/struct.js +156 -0
- package/vendor/toml/dist/util.js +57 -0
- package/vendor/toml/package.json +6 -0
- package/vendor/toml/provenance.json +35 -0
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import { loadNative } from './native.mjs';
|
|
3
|
+
const state = new WeakMap();
|
|
4
|
+
const finalizer = new FinalizationRegistry(fd => { try { fs.closeSync(fd); } catch {} });
|
|
5
|
+
function value(handle) {
|
|
6
|
+
const v = state.get(handle);
|
|
7
|
+
if (!v || v.fd === null) throw new Error('Filesystem handle is closed');
|
|
8
|
+
return v;
|
|
9
|
+
}
|
|
10
|
+
function register(handle, fd, native) {
|
|
11
|
+
state.set(handle, { fd, native }); finalizer.register(handle, fd, handle);
|
|
12
|
+
}
|
|
13
|
+
class Handle {
|
|
14
|
+
stat() { return fs.fstatSync(value(this).fd, { bigint: true }); }
|
|
15
|
+
chmod(mode) { fs.fchmodSync(value(this).fd, mode); }
|
|
16
|
+
setTimes(times, legacyMtime) {
|
|
17
|
+
const v = value(this);
|
|
18
|
+
if (times !== null && typeof times === 'object' && !(times instanceof Date)) {
|
|
19
|
+
if (typeof times.atimeNs !== 'bigint' || typeof times.mtimeNs !== 'bigint') throw new TypeError('Timestamp metadata requires BigInt nanoseconds');
|
|
20
|
+
v.native.setTimes(v.fd, times.atimeNs, times.mtimeNs);
|
|
21
|
+
} else fs.futimesSync(v.fd, times, legacyMtime);
|
|
22
|
+
}
|
|
23
|
+
sync() { fs.fsyncSync(value(this).fd); }
|
|
24
|
+
close() { const v = value(this); fs.closeSync(v.fd); v.fd = null; finalizer.unregister(this); }
|
|
25
|
+
}
|
|
26
|
+
export class FileHandle extends Handle {
|
|
27
|
+
readAll() { return this.read(); }
|
|
28
|
+
writeAll(buffer) { return this.write(buffer); }
|
|
29
|
+
read() {
|
|
30
|
+
const descriptor = value(this).fd;
|
|
31
|
+
const parts = []; let position = 0;
|
|
32
|
+
for (;;) {
|
|
33
|
+
const chunk = Buffer.alloc(65536);
|
|
34
|
+
const count = fs.readSync(descriptor, chunk, 0, chunk.length, position);
|
|
35
|
+
if (!count) return Buffer.concat(parts);
|
|
36
|
+
parts.push(chunk.subarray(0, count)); position += count;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
write(data, position = 0) {
|
|
40
|
+
const fd = value(this).fd; const bytes = Buffer.isBuffer(data) ? data : Buffer.from(data); let offset = 0;
|
|
41
|
+
while (offset < bytes.length) { const count = fs.writeSync(fd, bytes, offset, bytes.length - offset, position + offset); if (!count) throw new Error('Short file write'); offset += count; }
|
|
42
|
+
return offset;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
export class DirectoryHandle extends Handle {
|
|
46
|
+
openDir(name) { const v = value(this); const h = new DirectoryHandle(); register(h, v.native.openAt(v.fd, name, 1, 0, 0, 0, 0), v.native); return h; }
|
|
47
|
+
openFile(name, { read = true, create = false, exclusive = false, createExclusive = false, mode = 0o600 } = {}) {
|
|
48
|
+
if (!Number.isInteger(mode) || mode < 0 || mode > 0o7777) throw new TypeError('Invalid file mode');
|
|
49
|
+
const v = value(this), h = new FileHandle();
|
|
50
|
+
register(h, v.native.openAt(v.fd, name, 0, Number(create || createExclusive), Number(exclusive || createExclusive), mode, Number(!read || create || createExclusive)), v.native);
|
|
51
|
+
return h;
|
|
52
|
+
}
|
|
53
|
+
readDir() { const v = value(this); return v.native.readDir(v.fd).sort(); }
|
|
54
|
+
lstat(name) { const v = value(this); return v.native.lstat(v.fd, name); }
|
|
55
|
+
symlink(target, name) { const v = value(this); v.native.symlink(v.fd, name, target); }
|
|
56
|
+
readlink(name) { const v = value(this); return v.native.readlink(v.fd, name); }
|
|
57
|
+
mkdir(name, mode = 0o700) { if (!Number.isInteger(mode) || mode < 0 || mode > 0o7777) throw new TypeError('Invalid directory mode'); const v = value(this); v.native.mkdir(v.fd, name, mode); }
|
|
58
|
+
unlink(name) { const v = value(this); v.native.unlink(v.fd, name); }
|
|
59
|
+
rmdir(name) { const v = value(this); v.native.rmdir(v.fd, name); }
|
|
60
|
+
renameNoReplaceTo(name, destination, newName) { const v = value(this), target = value(destination); if (!(destination instanceof DirectoryHandle)) throw new TypeError('Destination must be a directory handle'); v.native.renameExclusive(v.fd, name, target.fd, newName); }
|
|
61
|
+
renameTo(name, destination, newName) { const v = value(this), target = value(destination); if (!(destination instanceof DirectoryHandle)) throw new TypeError('Destination must be a directory handle'); v.native.rename(v.fd, name, target.fd, newName); }
|
|
62
|
+
}
|
|
63
|
+
let defaultNative;
|
|
64
|
+
export function openRoot(path, { native } = {}) {
|
|
65
|
+
const binding = native ?? (defaultNative ??= loadNative());
|
|
66
|
+
const handle = new DirectoryHandle(); register(handle, binding.openRoot(path), binding); return handle;
|
|
67
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import os from 'node:os';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import crypto from 'node:crypto';
|
|
5
|
+
import { createRequire } from 'node:module';
|
|
6
|
+
import { fileURLToPath } from 'node:url';
|
|
7
|
+
const require = createRequire(import.meta.url);
|
|
8
|
+
const packageRoot = fileURLToPath(new URL('../../', import.meta.url));
|
|
9
|
+
export function hostRuntime() {
|
|
10
|
+
return { platform: process.platform, arch: process.arch, node: process.versions.node, release: os.release(),
|
|
11
|
+
glibc: process.report?.getReport().header.glibcVersionRuntime };
|
|
12
|
+
}
|
|
13
|
+
function runtimeError(message) { return Object.assign(new Error(message), { code: 'ARC_UNSUPPORTED_RUNTIME' }); }
|
|
14
|
+
export function supportedTuple(runtime = hostRuntime()) {
|
|
15
|
+
const major = Number(runtime.node?.split('.')[0]);
|
|
16
|
+
if (!Number.isInteger(major) || major < 22) throw runtimeError('Node.js 22 or later is required');
|
|
17
|
+
if (!['darwin', 'linux'].includes(runtime.platform) || !['arm64', 'x64'].includes(runtime.arch)) throw runtimeError('Unsupported operating system or CPU');
|
|
18
|
+
if (runtime.platform === 'darwin' && !(Number(runtime.release?.split('.')[0]) >= 22)) throw runtimeError('macOS 13 or later is required');
|
|
19
|
+
if (runtime.platform === 'linux') {
|
|
20
|
+
const [major, minor] = (runtime.glibc ?? '').split('.').map(Number);
|
|
21
|
+
if (!(major > 2 || major === 2 && minor >= 28)) throw runtimeError('glibc 2.28 or later is required');
|
|
22
|
+
}
|
|
23
|
+
return `${runtime.platform}-${runtime.arch}${runtime.platform === 'linux' ? '-gnu' : ''}`;
|
|
24
|
+
}
|
|
25
|
+
function verifyHeader(bytes, tuple) {
|
|
26
|
+
const [platform, arch] = tuple.split('-');
|
|
27
|
+
if (platform === 'darwin') {
|
|
28
|
+
if (bytes.length < 32 || bytes.readUInt32LE(0) !== 0xfeedfacf || bytes.readUInt32LE(4) !== (arch === 'arm64' ? 0x100000c : 0x1000007)) throw new Error('Invalid native Mach-O binary');
|
|
29
|
+
} else if (bytes.length < 64 || bytes.subarray(0, 4).toString('hex') !== '7f454c46' || bytes[4] !== 2 || bytes[5] !== 1 || bytes.readUInt16LE(18) !== (arch === 'arm64' ? 183 : 62)) throw new Error('Invalid native ELF binary');
|
|
30
|
+
}
|
|
31
|
+
export function loadNative({ root = packageRoot, runtime = hostRuntime(), artifactDirectory = 'prebuilds' } = {}) {
|
|
32
|
+
const tuple = supportedTuple(runtime);
|
|
33
|
+
try {
|
|
34
|
+
const artifactRoot = path.join(root, artifactDirectory);
|
|
35
|
+
let records;
|
|
36
|
+
try { records = JSON.parse(fs.readFileSync(path.join(artifactRoot, 'index.json'), 'utf8')).artifacts; }
|
|
37
|
+
catch (error) { throw new Error('Native artifact index unavailable: ' + error.message); }
|
|
38
|
+
const record = records?.[tuple];
|
|
39
|
+
if (!record || record.napi !== 8 || !/^[a-f0-9]{64}$/.test(record.sha256 ?? '')) throw new Error('Invalid native artifact record');
|
|
40
|
+
const binaryPath = path.join(artifactRoot, tuple, 'arc_safe_fs.node');
|
|
41
|
+
const descriptor = fs.openSync(binaryPath, fs.constants.O_RDONLY | fs.constants.O_NOFOLLOW | fs.constants.O_NONBLOCK);
|
|
42
|
+
let bytes;
|
|
43
|
+
try {
|
|
44
|
+
if (!fs.fstatSync(descriptor).isFile()) throw new Error('Native artifact is not a regular file');
|
|
45
|
+
bytes = fs.readFileSync(descriptor);
|
|
46
|
+
} finally { fs.closeSync(descriptor); }
|
|
47
|
+
if (crypto.createHash('sha256').update(bytes).digest('hex') !== record.sha256) throw new Error('Native artifact hash mismatch');
|
|
48
|
+
verifyHeader(bytes, tuple);
|
|
49
|
+
// The package is trusted executable code. Concurrent replacement of package
|
|
50
|
+
// code between verification and loading is outside the target-directory race
|
|
51
|
+
// protection boundary. Bootstrap performs no filesystem writes.
|
|
52
|
+
return require(binaryPath);
|
|
53
|
+
} catch (cause) { throw Object.assign(new Error(cause.message, { cause }), { code: 'ARC_NATIVE_UNAVAILABLE' }); }
|
|
54
|
+
}
|
package/native/binding.c
ADDED
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
#define _GNU_SOURCE
|
|
2
|
+
#include <node_api.h>
|
|
3
|
+
#include <sys/stat.h>
|
|
4
|
+
#include <fcntl.h>
|
|
5
|
+
#include <unistd.h>
|
|
6
|
+
#include <dirent.h>
|
|
7
|
+
#include <errno.h>
|
|
8
|
+
#include <stdlib.h>
|
|
9
|
+
#include <string.h>
|
|
10
|
+
#include <stdio.h>
|
|
11
|
+
#ifdef __linux__
|
|
12
|
+
#include <sys/syscall.h>
|
|
13
|
+
#include <linux/fs.h>
|
|
14
|
+
#endif
|
|
15
|
+
|
|
16
|
+
static napi_value fail(napi_env e, const char *what) {
|
|
17
|
+
char msg[256]; snprintf(msg,sizeof(msg),"%s: %s",what,strerror(errno));
|
|
18
|
+
const char *code="ERR_SAFE_FS";
|
|
19
|
+
switch(errno) {
|
|
20
|
+
#define ERRNO_CASE(x) case x: code=#x; break;
|
|
21
|
+
ERRNO_CASE(ENOENT) ERRNO_CASE(EEXIST) ERRNO_CASE(EACCES) ERRNO_CASE(EPERM)
|
|
22
|
+
ERRNO_CASE(ELOOP) ERRNO_CASE(ENOTDIR) ERRNO_CASE(EISDIR) ERRNO_CASE(EINVAL)
|
|
23
|
+
ERRNO_CASE(EBADF) ERRNO_CASE(ENOSPC) ERRNO_CASE(EXDEV) ERRNO_CASE(ENOTEMPTY) ERRNO_CASE(EIO)
|
|
24
|
+
}
|
|
25
|
+
napi_throw_error(e,code,msg); return NULL;
|
|
26
|
+
}
|
|
27
|
+
static int integer(napi_env e,napi_value v) { int32_t n=-1; napi_get_value_int32(e,v,&n); return n; }
|
|
28
|
+
static char *string(napi_env e,napi_value v) {
|
|
29
|
+
size_t n=0; if(napi_get_value_string_utf8(e,v,NULL,0,&n)!=napi_ok) { napi_throw_type_error(e,NULL,"Expected string"); return NULL; }
|
|
30
|
+
char *s=malloc(n+1); if(!s) { errno=ENOMEM; fail(e,"allocate"); return NULL; }
|
|
31
|
+
napi_get_value_string_utf8(e,v,s,n+1,&n);
|
|
32
|
+
if(strlen(s)!=n) { free(s); napi_throw_error(e,NULL,"NUL is forbidden"); return NULL; } return s;
|
|
33
|
+
}
|
|
34
|
+
static char *component(napi_env e,napi_value v) {
|
|
35
|
+
char *s=string(e,v); if(!s)return NULL;
|
|
36
|
+
if(!*s||!strcmp(s,".")||!strcmp(s,"..")||strchr(s,'/')) { free(s); napi_throw_error(e,NULL,"Expected one safe path component"); return NULL; } return s;
|
|
37
|
+
}
|
|
38
|
+
static napi_value number(napi_env e,int n) { napi_value v; napi_create_int32(e,n,&v); return v; }
|
|
39
|
+
static napi_value open_root(napi_env e,napi_callback_info info) {
|
|
40
|
+
napi_value a[1]; size_t n=1; napi_get_cb_info(e,info,&n,a,NULL,NULL);
|
|
41
|
+
char *p=string(e,a[0]); if(!p)return NULL;
|
|
42
|
+
if(p[0]!='/') { free(p); napi_throw_error(e,NULL,"Root must be absolute"); return NULL; }
|
|
43
|
+
int fd=open("/",O_RDONLY|O_DIRECTORY|O_CLOEXEC); if(fd<0) { free(p); return fail(e,"open root"); }
|
|
44
|
+
char *save=NULL; char *part=strtok_r(p,"/",&save);
|
|
45
|
+
while(part) {
|
|
46
|
+
if(!strcmp(part,".")||!strcmp(part,"..")) { close(fd); free(p); napi_throw_error(e,NULL,"Unsafe root component"); return NULL; }
|
|
47
|
+
int next=openat(fd,part,O_RDONLY|O_DIRECTORY|O_NOFOLLOW|O_CLOEXEC); int saved=errno; close(fd);
|
|
48
|
+
if(next<0) { free(p); errno=saved; return fail(e,"open root component"); } fd=next; part=strtok_r(NULL,"/",&save);
|
|
49
|
+
}
|
|
50
|
+
free(p); return number(e,fd);
|
|
51
|
+
}
|
|
52
|
+
static napi_value open_at(napi_env e,napi_callback_info info) {
|
|
53
|
+
napi_value a[7]; size_t n=7; napi_get_cb_info(e,info,&n,a,NULL,NULL);
|
|
54
|
+
if(n<7) { napi_throw_type_error(e,NULL,"Invalid native open arguments"); return NULL; }
|
|
55
|
+
int parent=integer(e,a[0]); char *name=component(e,a[1]); if(!name)return NULL;
|
|
56
|
+
int directory=integer(e,a[2]), create=integer(e,a[3]), exclusive=integer(e,a[4]);
|
|
57
|
+
int flags=O_CLOEXEC|O_NOFOLLOW|O_NONBLOCK|(directory ? O_RDONLY|O_DIRECTORY : (integer(e,a[6]) ? O_RDWR : O_RDONLY));
|
|
58
|
+
if(create) flags |= O_CREAT;
|
|
59
|
+
if(exclusive) flags |= O_EXCL;
|
|
60
|
+
int fd=openat(parent,name,flags,(mode_t)integer(e,a[5])); int saved=errno; free(name);
|
|
61
|
+
if(fd<0) { errno=saved; return fail(e,"open component"); }
|
|
62
|
+
struct stat st; if(fstat(fd,&st)<0 || (directory ? !S_ISDIR(st.st_mode) : !S_ISREG(st.st_mode))) { close(fd); errno=EINVAL; return fail(e,"Unexpected file type"); }
|
|
63
|
+
return number(e,fd);
|
|
64
|
+
}
|
|
65
|
+
static napi_value mutate(napi_env e,napi_callback_info info) {
|
|
66
|
+
napi_value a[5]; size_t n=5; void *data; napi_get_cb_info(e,info,&n,a,NULL,&data);
|
|
67
|
+
int fd=integer(e,a[0]); char *name=component(e,a[1]); if(!name)return NULL;
|
|
68
|
+
intptr_t op=(intptr_t)data; int rc=-1;
|
|
69
|
+
if(op==0) rc=mkdirat(fd,name,(mode_t)integer(e,a[2]));
|
|
70
|
+
else if(op==1||op==2)rc=unlinkat(fd,name,op==2?AT_REMOVEDIR:0);
|
|
71
|
+
else { char *dest=component(e,a[3]); if(!dest) { free(name); return NULL; } rc=renameat(fd,name,integer(e,a[2]),dest); free(dest); }
|
|
72
|
+
int saved=errno; free(name); if(rc<0) { errno=saved; return fail(e,"relative mutation"); } return number(e,0);
|
|
73
|
+
}
|
|
74
|
+
static napi_value rename_exclusive(napi_env e,napi_callback_info info) {
|
|
75
|
+
napi_value a[4]; size_t n=4; napi_get_cb_info(e,info,&n,a,NULL,NULL);
|
|
76
|
+
if(n!=4) { napi_throw_type_error(e,NULL,"Expected source and destination handles and names"); return NULL; }
|
|
77
|
+
char *source=component(e,a[1]); if(!source)return NULL;
|
|
78
|
+
char *destination=component(e,a[3]); if(!destination) { free(source); return NULL; }
|
|
79
|
+
int rc;
|
|
80
|
+
#ifdef __APPLE__
|
|
81
|
+
rc=renameatx_np(integer(e,a[0]),source,integer(e,a[2]),destination,RENAME_EXCL);
|
|
82
|
+
#elif defined(__linux__) && defined(SYS_renameat2)
|
|
83
|
+
rc=(int)syscall(SYS_renameat2,integer(e,a[0]),source,integer(e,a[2]),destination,RENAME_NOREPLACE);
|
|
84
|
+
#else
|
|
85
|
+
errno=ENOTSUP; rc=-1;
|
|
86
|
+
#endif
|
|
87
|
+
int saved=errno; free(source); free(destination);
|
|
88
|
+
if(rc<0) { errno=saved; return fail(e,"atomic no-replace rename"); }
|
|
89
|
+
return number(e,0);
|
|
90
|
+
}
|
|
91
|
+
static napi_value set_times(napi_env e,napi_callback_info info) {
|
|
92
|
+
napi_value a[3]; size_t n=3; napi_get_cb_info(e,info,&n,a,NULL,NULL);
|
|
93
|
+
if(n<3) { napi_throw_type_error(e,NULL,"Expected descriptor and nanosecond times"); return NULL; }
|
|
94
|
+
struct timespec times[2];
|
|
95
|
+
for(int i=0;i<2;i++) {
|
|
96
|
+
int64_t ns; bool lossless;
|
|
97
|
+
if(napi_get_value_bigint_int64(e,a[i+1],&ns,&lossless)!=napi_ok || !lossless) {
|
|
98
|
+
napi_throw_type_error(e,NULL,"Timestamp must be a signed 64-bit BigInt in nanoseconds"); return NULL;
|
|
99
|
+
}
|
|
100
|
+
times[i].tv_sec=ns/1000000000; times[i].tv_nsec=ns%1000000000;
|
|
101
|
+
if(times[i].tv_nsec<0) { times[i].tv_sec--; times[i].tv_nsec+=1000000000; }
|
|
102
|
+
}
|
|
103
|
+
if(futimens(integer(e,a[0]),times)<0)return fail(e,"set descriptor timestamps");
|
|
104
|
+
return number(e,0);
|
|
105
|
+
}
|
|
106
|
+
static napi_value symlink_at(napi_env e,napi_callback_info info) {
|
|
107
|
+
napi_value a[3]; size_t n=3; napi_get_cb_info(e,info,&n,a,NULL,NULL);
|
|
108
|
+
char *name=component(e,a[1]); if(!name)return NULL;
|
|
109
|
+
char *target=string(e,a[2]); if(!target) { free(name); return NULL; }
|
|
110
|
+
int rc=symlinkat(target,integer(e,a[0]),name); int saved=errno; free(name); free(target);
|
|
111
|
+
if(rc<0) { errno=saved; return fail(e,"create symlink"); } return number(e,0);
|
|
112
|
+
}
|
|
113
|
+
static napi_value read_link(napi_env e,napi_callback_info info) {
|
|
114
|
+
napi_value a[2],v; size_t n=2; napi_get_cb_info(e,info,&n,a,NULL,NULL); char *s=component(e,a[1]); if(!s)return NULL;
|
|
115
|
+
char buf[65536]; ssize_t len=readlinkat(integer(e,a[0]),s,buf,sizeof(buf)); int saved=errno; free(s);
|
|
116
|
+
if(len<0) { errno=saved; return fail(e,"readlink"); } if(len==sizeof(buf)) { errno=ENAMETOOLONG; return fail(e,"readlink"); }
|
|
117
|
+
napi_create_string_utf8(e,buf,len,&v); return v;
|
|
118
|
+
}
|
|
119
|
+
static napi_value list(napi_env e,napi_callback_info info) {
|
|
120
|
+
napi_value a[1],result; size_t n=1; napi_get_cb_info(e,info,&n,a,NULL,NULL);
|
|
121
|
+
int fd=openat(integer(e,a[0]),".",O_RDONLY|O_DIRECTORY|O_CLOEXEC); if(fd<0)return fail(e,"read directory");
|
|
122
|
+
DIR *d=fdopendir(fd); if(!d) { close(fd); return fail(e,"read directory"); }
|
|
123
|
+
napi_create_array(e,&result); unsigned i=0; struct dirent *entry; int saved=0;
|
|
124
|
+
for(;;) {
|
|
125
|
+
/* N-API conversion may change errno. Only readdir's EOF/error owns it. */
|
|
126
|
+
errno=0; entry=readdir(d); if(!entry) { saved=errno; break; }
|
|
127
|
+
if(!strcmp(entry->d_name,".")||!strcmp(entry->d_name,".."))continue;
|
|
128
|
+
napi_value v; napi_create_string_utf8(e,entry->d_name,NAPI_AUTO_LENGTH,&v); napi_set_element(e,result,i++,v);
|
|
129
|
+
}
|
|
130
|
+
closedir(d); if(saved) { errno=saved; return fail(e,"read directory"); } return result;
|
|
131
|
+
}
|
|
132
|
+
static napi_value stat_at(napi_env e,napi_callback_info info) {
|
|
133
|
+
napi_value a[2],o; size_t n=2; napi_get_cb_info(e,info,&n,a,NULL,NULL); char *s=component(e,a[1]); if(!s)return NULL;
|
|
134
|
+
struct stat st; int rc=fstatat(integer(e,a[0]),s,&st,AT_SYMLINK_NOFOLLOW); int saved=errno; free(s); if(rc<0) { errno=saved; return fail(e,"lstat"); }
|
|
135
|
+
napi_create_object(e,&o);
|
|
136
|
+
#define FIELD(k,value) { napi_value v; napi_create_bigint_int64(e,(int64_t)(value),&v); napi_set_named_property(e,o,k,v); }
|
|
137
|
+
FIELD("dev",st.st_dev); FIELD("ino",st.st_ino); FIELD("mode",st.st_mode); FIELD("size",st.st_size);
|
|
138
|
+
FIELD("nlink",st.st_nlink); FIELD("uid",st.st_uid); FIELD("gid",st.st_gid);
|
|
139
|
+
#ifdef __APPLE__
|
|
140
|
+
int64_t atime=(int64_t)st.st_atimespec.tv_sec*1000000000+st.st_atimespec.tv_nsec;
|
|
141
|
+
int64_t mtime=(int64_t)st.st_mtimespec.tv_sec*1000000000+st.st_mtimespec.tv_nsec;
|
|
142
|
+
int64_t ctime=(int64_t)st.st_ctimespec.tv_sec*1000000000+st.st_ctimespec.tv_nsec;
|
|
143
|
+
FIELD("birthtimeNs",(int64_t)st.st_birthtimespec.tv_sec*1000000000+st.st_birthtimespec.tv_nsec);
|
|
144
|
+
#else
|
|
145
|
+
int64_t atime=(int64_t)st.st_atim.tv_sec*1000000000+st.st_atim.tv_nsec;
|
|
146
|
+
int64_t mtime=(int64_t)st.st_mtim.tv_sec*1000000000+st.st_mtim.tv_nsec;
|
|
147
|
+
int64_t ctime=(int64_t)st.st_ctim.tv_sec*1000000000+st.st_ctim.tv_nsec;
|
|
148
|
+
#endif
|
|
149
|
+
FIELD("atimeNs",atime); FIELD("mtimeNs",mtime); FIELD("ctimeNs",ctime);
|
|
150
|
+
FIELD("atimeMs",atime/1000000); FIELD("mtimeMs",mtime/1000000); FIELD("ctimeMs",ctime/1000000);
|
|
151
|
+
return o;
|
|
152
|
+
}
|
|
153
|
+
static napi_value init(napi_env e,napi_value exports) {
|
|
154
|
+
struct { const char *name; napi_callback cb; intptr_t data; } f[]={ {"openRoot",open_root,0},{"openAt",open_at,0},{"mkdir",mutate,0},{"unlink",mutate,1},{"rmdir",mutate,2},{"rename",mutate,3},{"renameExclusive",rename_exclusive,0},{"setTimes",set_times,0},{"symlink",symlink_at,0},{"readlink",read_link,0},{"readDir",list,0},{"lstat",stat_at,0} };
|
|
155
|
+
for(unsigned i=0;i<sizeof(f)/sizeof(f[0]);i++) { napi_value v; napi_create_function(e,f[i].name,NAPI_AUTO_LENGTH,f[i].cb,(void*)f[i].data,&v); napi_set_named_property(e,exports,f[i].name,v); } return exports;
|
|
156
|
+
}
|
|
157
|
+
NAPI_MODULE(NODE_GYP_MODULE_NAME,init)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"targets":[{"target_name":"arc_safe_fs","sources":["binding.c"],"defines":["NAPI_VERSION=8"],"xcode_settings":{"MACOSX_DEPLOYMENT_TARGET":"13.0"}}]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,45 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yunhozz/arc",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
|
|
6
|
-
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"publishConfig": {
|
|
5
|
+
"access": "public",
|
|
6
|
+
"registry": "https://registry.npmjs.org/"
|
|
7
|
+
},
|
|
8
|
+
"type": "module",
|
|
9
|
+
"engines": {
|
|
10
|
+
"node": ">=22"
|
|
11
|
+
},
|
|
12
|
+
"scripts": {
|
|
13
|
+
"test": "node --test tests/node/*.test.mjs",
|
|
14
|
+
"build:native": "node scripts/build-native.mjs",
|
|
15
|
+
"check:manifest": "node scripts/update-manifest.mjs --check",
|
|
16
|
+
"update:role-policy": "node scripts/update-role-policy.mjs",
|
|
17
|
+
"check:role-policy": "node scripts/update-role-policy.mjs --check"
|
|
18
|
+
},
|
|
19
|
+
"devDependencies": {
|
|
20
|
+
"node-gyp": "11.5.0",
|
|
21
|
+
"smol-toml": "1.9.1"
|
|
22
|
+
},
|
|
23
|
+
"bin": {
|
|
24
|
+
"arc": "bin/arc.mjs"
|
|
25
|
+
},
|
|
26
|
+
"files": [
|
|
27
|
+
"bin/",
|
|
28
|
+
"lib/",
|
|
29
|
+
"scripts/",
|
|
30
|
+
"vendor/",
|
|
31
|
+
"prebuilds/",
|
|
32
|
+
"native/binding.c",
|
|
33
|
+
"native/binding.gyp",
|
|
34
|
+
"codex/skill/",
|
|
35
|
+
"codex/agents/",
|
|
36
|
+
"claude/skill/",
|
|
37
|
+
"claude/agents/",
|
|
38
|
+
"MANIFEST.sha256",
|
|
39
|
+
"install.command"
|
|
40
|
+
],
|
|
41
|
+
"repository": {
|
|
42
|
+
"type": "git",
|
|
43
|
+
"url": "git+https://github.com/yunhozz/arc.git"
|
|
44
|
+
}
|
|
45
|
+
}
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
{
|
|
2
|
+
"artifacts": {
|
|
3
|
+
"darwin-arm64": {
|
|
4
|
+
"sha256": "208c65cfde244d39ce45c3cf1b0acb128f04d21199b6b3fb5466e62db5b64af9",
|
|
5
|
+
"sourceCommit": "f4386726e133922676aeb2f8d9d73b2b87afbf96",
|
|
6
|
+
"nativeInputDigest": "831a685a17634789f84f9f16077e2f0ce28f79e99714d54b7bc122b40b5139cf",
|
|
7
|
+
"napi": 8,
|
|
8
|
+
"nodeHeaders": "22.23.2",
|
|
9
|
+
"nodeHeadersDigest": "6f8b5e0047e9d15a28da9aead4236cebf5171031359b2f6a1d9fdf05f8f04bf3",
|
|
10
|
+
"sourceDirty": false,
|
|
11
|
+
"buildImage": null,
|
|
12
|
+
"libc": null,
|
|
13
|
+
"compiler": "Apple clang version 17.0.0 (clang-1700.0.13.5)\nTarget: arm64-apple-darwin24.6.0\nThread model: posix\nInstalledDir: /Applications/Xcode_16.4.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin",
|
|
14
|
+
"sysroot": "/Applications/Xcode_16.4.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk",
|
|
15
|
+
"deploymentTarget": "13.0",
|
|
16
|
+
"tuple": "darwin-arm64"
|
|
17
|
+
},
|
|
18
|
+
"darwin-x64": {
|
|
19
|
+
"sha256": "572887ac794d10a8b921d8168ea2328e31f05b45987611ff02c83114dba901af",
|
|
20
|
+
"sourceCommit": "f4386726e133922676aeb2f8d9d73b2b87afbf96",
|
|
21
|
+
"nativeInputDigest": "831a685a17634789f84f9f16077e2f0ce28f79e99714d54b7bc122b40b5139cf",
|
|
22
|
+
"napi": 8,
|
|
23
|
+
"nodeHeaders": "22.23.2",
|
|
24
|
+
"nodeHeadersDigest": "6f8b5e0047e9d15a28da9aead4236cebf5171031359b2f6a1d9fdf05f8f04bf3",
|
|
25
|
+
"sourceDirty": false,
|
|
26
|
+
"buildImage": null,
|
|
27
|
+
"libc": null,
|
|
28
|
+
"compiler": "Apple clang version 17.0.0 (clang-1700.0.13.5)\nTarget: x86_64-apple-darwin24.6.0\nThread model: posix\nInstalledDir: /Applications/Xcode_16.4.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin",
|
|
29
|
+
"sysroot": "/Applications/Xcode_16.4.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk",
|
|
30
|
+
"deploymentTarget": "13.0",
|
|
31
|
+
"tuple": "darwin-x64"
|
|
32
|
+
},
|
|
33
|
+
"linux-arm64-gnu": {
|
|
34
|
+
"sha256": "b98bb4ac757ebd6e8f8fc1eb7a6a4389e3a8199984baf6bb1c327a6f952c2c0d",
|
|
35
|
+
"sourceCommit": "f4386726e133922676aeb2f8d9d73b2b87afbf96",
|
|
36
|
+
"nativeInputDigest": "831a685a17634789f84f9f16077e2f0ce28f79e99714d54b7bc122b40b5139cf",
|
|
37
|
+
"napi": 8,
|
|
38
|
+
"nodeHeaders": "22.23.3",
|
|
39
|
+
"nodeHeadersDigest": "b1c10d710bb872f0d5d9f53756bb0c6cebe4b6441722268a7a2d0889e1df59ce",
|
|
40
|
+
"sourceDirty": false,
|
|
41
|
+
"buildImage": "quay.io/pypa/manylinux_2_28_aarch64@sha256:abd1230aa9e4058a74b89d27cd14ba18c0db036d7bfe6e6f219a29ea63c24eb6",
|
|
42
|
+
"libc": "glibc 2.28",
|
|
43
|
+
"compiler": "cc (GCC) 14.2.1 20250110 (Red Hat 14.2.1-11)\nCopyright (C) 2024 Free Software Foundation, Inc.\nThis is free software; see the source for copying conditions. There is NO\nwarranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.",
|
|
44
|
+
"sysroot": "/",
|
|
45
|
+
"deploymentTarget": "glibc-2.28",
|
|
46
|
+
"tuple": "linux-arm64-gnu"
|
|
47
|
+
},
|
|
48
|
+
"linux-x64-gnu": {
|
|
49
|
+
"sha256": "9f8c2c1e328ee287355bc36afc3173c00ed062611fbd1262e4d30fca83e979dc",
|
|
50
|
+
"sourceCommit": "f4386726e133922676aeb2f8d9d73b2b87afbf96",
|
|
51
|
+
"nativeInputDigest": "831a685a17634789f84f9f16077e2f0ce28f79e99714d54b7bc122b40b5139cf",
|
|
52
|
+
"napi": 8,
|
|
53
|
+
"nodeHeaders": "22.23.3",
|
|
54
|
+
"nodeHeadersDigest": "b1c10d710bb872f0d5d9f53756bb0c6cebe4b6441722268a7a2d0889e1df59ce",
|
|
55
|
+
"sourceDirty": false,
|
|
56
|
+
"buildImage": "quay.io/pypa/manylinux_2_28_x86_64@sha256:4131ddd7acb9c6c966af02fe7e71edc51e078c2d41f475966697676430bfa2ae",
|
|
57
|
+
"libc": "glibc 2.28",
|
|
58
|
+
"compiler": "cc (GCC) 14.2.1 20250110 (Red Hat 14.2.1-11)\nCopyright (C) 2024 Free Software Foundation, Inc.\nThis is free software; see the source for copying conditions. There is NO\nwarranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.",
|
|
59
|
+
"sysroot": "/",
|
|
60
|
+
"deploymentTarget": "glibc-2.28",
|
|
61
|
+
"tuple": "linux-x64-gnu"
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import crypto from 'node:crypto';
|
|
4
|
+
import { spawnSync } from 'node:child_process';
|
|
5
|
+
import { fileURLToPath } from 'node:url';
|
|
6
|
+
import { supportedTuple } from '../lib/safe-fs/native.mjs';
|
|
7
|
+
const root = fileURLToPath(new URL('../', import.meta.url));
|
|
8
|
+
function argument(name, fallback) { const i = process.argv.indexOf(name); return i < 0 ? fallback : process.argv[i + 1]; }
|
|
9
|
+
const tuple = argument('--tuple', supportedTuple());
|
|
10
|
+
if (tuple !== supportedTuple()) throw new Error('Native builds must run on the selected target tuple');
|
|
11
|
+
const output = path.resolve(argument('--output', `build/native/${tuple}`));
|
|
12
|
+
const headerCache = path.join(root, 'build/node-headers');
|
|
13
|
+
const build = spawnSync(process.execPath, [path.join(root, 'node_modules/node-gyp/bin/node-gyp.js'), 'rebuild', '--devdir', headerCache, '--directory', path.join(root, 'native')], { stdio: 'inherit', env: { ...process.env, MACOSX_DEPLOYMENT_TARGET: '13.0' } });
|
|
14
|
+
if (build.status !== 0) process.exit(build.status ?? 1);
|
|
15
|
+
const binary = fs.readFileSync(path.join(root, 'native/build/Release/arc_safe_fs.node'));
|
|
16
|
+
const digest = crypto.createHash('sha256');
|
|
17
|
+
for (const name of ['native/binding.c', 'native/binding.gyp', 'scripts/build-native.mjs', 'package-lock.json']) digest.update(name).update('\0').update(fs.readFileSync(path.join(root, name))).update('\0');
|
|
18
|
+
const command = (cmd, args) => { const r = spawnSync(cmd, args, { cwd: root, encoding: 'utf8' }); if (r.status !== 0) throw new Error(`Cannot identify build input: ${cmd}`); return r.stdout.trim(); };
|
|
19
|
+
const headerDigest = crypto.createHash('sha256');
|
|
20
|
+
function hashTree(directory, relative = '') {
|
|
21
|
+
for (const name of fs.readdirSync(path.join(directory, relative)).sort()) {
|
|
22
|
+
const child = path.join(relative, name); const filename = path.join(directory, child);
|
|
23
|
+
if (fs.lstatSync(filename).isDirectory()) hashTree(directory, child);
|
|
24
|
+
else headerDigest.update(child).update('\0').update(fs.readFileSync(filename)).update('\0');
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
hashTree(path.join(headerCache, process.versions.node, 'include/node'));
|
|
28
|
+
const libc = process.platform === 'linux' ? command('getconf', ['GNU_LIBC_VERSION']) : null;
|
|
29
|
+
if (libc && libc !== 'glibc 2.28') throw new Error('Release Linux native builds require the glibc 2.28 build image');
|
|
30
|
+
const record = { sha256: crypto.createHash('sha256').update(binary).digest('hex'), sourceCommit: command('git', ['rev-parse', 'HEAD']), nativeInputDigest: digest.digest('hex'), napi: 8, nodeHeaders: process.versions.node, nodeHeadersDigest: headerDigest.digest('hex'), sourceDirty: Boolean(command('git', ['status', '--porcelain', '--untracked-files=no'])), buildImage: process.env.ARC_BUILD_IMAGE ?? null, libc, compiler: command('cc', ['--version']), sysroot: process.platform === 'darwin' ? command('xcrun', ['--show-sdk-path']) : command('cc', ['-print-sysroot']) || '/', deploymentTarget: process.platform === 'darwin' ? '13.0' : 'glibc-2.28', tuple };
|
|
31
|
+
fs.mkdirSync(output, { recursive: true }); fs.writeFileSync(path.join(output, 'arc_safe_fs.node'), binary);
|
|
32
|
+
fs.writeFileSync(path.join(output, 'provenance.json'), JSON.stringify(record, null, 2) + '\n');
|
|
33
|
+
const base = path.dirname(output); const indexFile = path.join(base, 'index.json');
|
|
34
|
+
const index = fs.existsSync(indexFile) ? JSON.parse(fs.readFileSync(indexFile)) : { artifacts: {} };
|
|
35
|
+
index.artifacts[tuple] = record; fs.writeFileSync(indexFile, JSON.stringify(index, null, 2) + '\n');
|
|
36
|
+
console.log(`Built ${tuple}: ${record.sha256}`);
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import crypto from 'node:crypto';
|
|
4
|
+
import {spawnSync} from 'node:child_process';
|
|
5
|
+
import {fileURLToPath} from 'node:url';
|
|
6
|
+
const arg=(name,fallback)=>{const i=process.argv.indexOf(name);return i<0?fallback:process.argv[i+1];};
|
|
7
|
+
const root=path.resolve(arg('--root',fileURLToPath(new URL('../',import.meta.url))));
|
|
8
|
+
const complete=process.argv.includes('--complete');
|
|
9
|
+
// Frozen before the legacy files were removed: test bodies, assertion expressions,
|
|
10
|
+
// and helper assertion sites. Coverage mappings may change; this inventory may not.
|
|
11
|
+
const inventorySha='b21f21cb1786d8400914f6f1837e8c3dddc51e4629fbdc4e091567a99f3825de';
|
|
12
|
+
try{
|
|
13
|
+
const ledger=JSON.parse(fs.readFileSync(path.join(root,'tests/installer-parity.json'),'utf8'));
|
|
14
|
+
const inventory={tests:ledger.legacyInventory,helpers:ledger.legacyHelpers?.map(({id,assertions})=>({id,assertions}))};
|
|
15
|
+
if(crypto.createHash('sha256').update(JSON.stringify(inventory)).digest('hex')!==inventorySha)throw Error('Original assertion inventory changed');
|
|
16
|
+
if(inventory.tests.length!==236||inventory.tests.reduce((n,t)=>n+t.assertions.length,0)!==739||inventory.helpers.reduce((n,t)=>n+t.assertions.length,0)!==14)throw Error('Original assertion inventory count changed');
|
|
17
|
+
const entryIds=ledger.entries.map(e=>e.legacy);if(new Set(entryIds).size!==entryIds.length)throw Error('Duplicate legacy assertion ID');if(entryIds.length!==236)throw Error('Original inventory ID count changed');
|
|
18
|
+
let retiredIds=0,retiredAssertions=0;
|
|
19
|
+
const original=new Map(inventory.tests.map(t=>[t.id,t])),seen=new Set(),destinations=new Map();
|
|
20
|
+
const addDestination=(destination,owner)=>{
|
|
21
|
+
if(!/^tests\/(?:node\/[^/]+\.test\.mjs|test_[a-z_]+\.py)$/.test(destination?.file)||typeof destination.test!=='string'||!destination.test.trim())throw Error(`Invalid destination: ${owner}`);
|
|
22
|
+
if(!fs.existsSync(path.join(root,destination.file)))throw Error(`Missing mapped test file: ${destination.file} (${owner})`);
|
|
23
|
+
destinations.set(JSON.stringify(destination),destination);
|
|
24
|
+
};
|
|
25
|
+
for(const entry of ledger.entries){
|
|
26
|
+
if(seen.has(entry.legacy))throw Error(`Duplicate legacy assertion: ${entry.legacy}`);seen.add(entry.legacy);
|
|
27
|
+
const before=original.get(entry.legacy);if(!before)throw Error(`Unknown original inventory ID: ${entry.legacy}`);
|
|
28
|
+
if(entry.status==='retired'){
|
|
29
|
+
if(entry.legacy!=='tests/test_install.py::PolicyWordBudgetTests::test_policy_text_stays_within_word_budget'||entry.approvedPlan!=='docs/superpowers/plans/2026-10-17-arc-harness-refactor-plan.md'||!fs.existsSync(path.join(root,entry.approvedPlan))||typeof entry.retirementReason!=='string'||!entry.retirementReason.trim()||!Array.isArray(entry.destinations)||entry.destinations.length||Object.hasOwn(entry,'destination'))throw Error(`Invalid retired legacy assertion: ${entry.legacy}`);
|
|
30
|
+
if(JSON.stringify(entry.assertionLines)!==JSON.stringify(before.assertions.map(a=>a.line)))throw Error(`Changed retired assertion coverage: ${entry.legacy}`);
|
|
31
|
+
retiredIds++;retiredAssertions+=before.assertions.length;continue;
|
|
32
|
+
}
|
|
33
|
+
if(entry.status!=='verified')throw Error(`Unverified legacy assertion: ${entry.legacy}`);
|
|
34
|
+
const coverage=entry.assertionLines;if(!coverage||coverage.length!==before.assertions.length||!entry.destinations?.length)throw Error(`Incomplete assertion coverage: ${entry.legacy}`);
|
|
35
|
+
for(const assertion of before.assertions)if(!coverage.includes(assertion.line))throw Error(`Missing assertion destination: ${entry.legacy}:${assertion.line}`);
|
|
36
|
+
for(const destination of entry.destinations)addDestination(destination,entry.legacy);
|
|
37
|
+
}
|
|
38
|
+
for(const id of original.keys())if(!seen.has(id))throw Error(`Missing original inventory ID: ${id}`);
|
|
39
|
+
for(const helper of ledger.legacyHelpers){
|
|
40
|
+
if(!helper.destinations?.length)throw Error(`Missing helper assertion coverage: ${helper.id}`);
|
|
41
|
+
for(const destination of helper.destinations)addDestination(destination,helper.id);
|
|
42
|
+
}
|
|
43
|
+
// While the originals exist, detect a source inventory change as well.
|
|
44
|
+
const sourceIds=new Set();for(const filename of ['tests/test_install.py','tests/test_target_install.py']){const file=path.join(root,filename);if(!fs.existsSync(file))continue;let group;for(const line of fs.readFileSync(file,'utf8').split('\n')){const cls=/^class (\w+)/.exec(line);if(cls)group=cls[1];const method=/^ def (test_\w+)\(/.exec(line);if(method)sourceIds.add(`${filename}::${group}::${method[1]}`);}}for(const id of sourceIds)if(!original.has(id))throw Error(`Uninventoried legacy test: ${id}`);
|
|
45
|
+
if(complete){
|
|
46
|
+
const all=[...destinations.values()],nodeFiles=[...new Set(all.filter(d=>d.file.endsWith('.mjs')).map(d=>d.file))];
|
|
47
|
+
const escape=text=>text.replace(/[.*+?^${}()|[\]\\]/g,'\\$&');
|
|
48
|
+
for(const file of nodeFiles){
|
|
49
|
+
const selected=all.filter(d=>d.file===file),pattern='^(?:'+selected.map(d=>escape(d.test)).join('|')+')$';
|
|
50
|
+
const result=spawnSync(process.execPath,['--test','--test-reporter=tap','--test-name-pattern',pattern,file],{cwd:root,encoding:'utf8',maxBuffer:16*1024*1024});
|
|
51
|
+
if(result.status!==0)throw Error(`Mapped Node tests failed: ${file}\n${result.stdout}\n${result.stderr}`);
|
|
52
|
+
const passed=new Set([...result.stdout.matchAll(/^ok \d+ - (.+)$/gm)].map(m=>m[1]));
|
|
53
|
+
for(const d of selected)if(!passed.has(d.test))throw Error(`Mapped Node destination did not execute: ${file}: ${d.test}`);
|
|
54
|
+
}
|
|
55
|
+
const pythonFiles=[...new Set(all.filter(d=>d.file.endsWith('.py')).map(d=>d.file))];
|
|
56
|
+
for(const file of pythonFiles){const result=spawnSync('python3',['-B','-m','unittest','discover','-v','-s','tests','-p',path.basename(file)],{cwd:root,encoding:'utf8',env:{...process.env,PYTHONDONTWRITEBYTECODE:'1'}});if(result.status!==0)throw Error(`Preserved Python suite failed: ${file}\n${result.stderr}`);for(const d of all.filter(d=>d.file===file))if(!result.stderr.includes(`.${d.class}.${d.test}) ... ok`))throw Error(`Preserved destination did not execute: ${file}: ${d.class}.${d.test}`);}
|
|
57
|
+
}
|
|
58
|
+
console.log(`Parity verified: 236 original test IDs (${236-retiredIds} verified + ${retiredIds} retired), 739 test assertions (${739-retiredAssertions} verified + ${retiredAssertions} retired), 14 helper assertions${complete?'; all active mappings ran; every mapped Node and preserved Python test passed':''}`);
|
|
59
|
+
}catch(error){console.error(error.message);process.exitCode=1;}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import {isDynamicReferencePath} from '../lib/installer/superpowers.mjs';
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import crypto from 'node:crypto';
|
|
5
|
+
import { fileURLToPath } from 'node:url';
|
|
6
|
+
const i = process.argv.indexOf('--root');
|
|
7
|
+
const root = i < 0 ? fileURLToPath(new URL('../', import.meta.url)) : path.resolve(process.argv[i + 1]);
|
|
8
|
+
const mutable = new Set(['codex/skill/models.toml', 'claude/skill/models.toml']);
|
|
9
|
+
const excluded = new Set(['.DS_Store', '__pycache__', 'build']);
|
|
10
|
+
const files = new Set();
|
|
11
|
+
function visit(relative) {
|
|
12
|
+
if(isDynamicReferencePath(relative)) return;
|
|
13
|
+
const filename = path.join(root, relative);
|
|
14
|
+
if (!fs.existsSync(filename)) return;
|
|
15
|
+
const stat = fs.lstatSync(filename);
|
|
16
|
+
if (stat.isSymbolicLink()) throw new Error(`Protected payload contains symlink: ${relative}`);
|
|
17
|
+
if (stat.isDirectory()) {
|
|
18
|
+
for (const name of fs.readdirSync(filename).sort()) if (!excluded.has(name)) visit(`${relative}/${name}`);
|
|
19
|
+
} else if (stat.isFile() && !mutable.has(relative)) files.add(relative);
|
|
20
|
+
else if (!stat.isFile()) throw new Error(`Protected payload is not regular: ${relative}`);
|
|
21
|
+
}
|
|
22
|
+
for (const entry of ['README.md', 'install.command', 'package.json', 'bin', 'lib', 'native/binding.c', 'native/binding.gyp', 'scripts', 'vendor', 'prebuilds', 'codex/skill', 'codex/agents', 'claude/skill', 'claude/agents']) visit(entry);
|
|
23
|
+
const content = [...files].sort().map(name => `${crypto.createHash('sha256').update(fs.readFileSync(path.join(root, name))).digest('hex')} ${name}\n`).join('');
|
|
24
|
+
const manifest = path.join(root, 'MANIFEST.sha256');
|
|
25
|
+
if (process.argv.includes('--check')) {
|
|
26
|
+
if (!fs.existsSync(manifest) || fs.readFileSync(manifest, 'utf8') !== content) { console.error('Protected payload manifest is stale'); process.exitCode = 1; }
|
|
27
|
+
else console.log(`Protected payload manifest verified (${files.size} files)`);
|
|
28
|
+
} else { fs.writeFileSync(manifest, content); console.log(`Updated manifest (${files.size} files)`); }
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import {fileURLToPath} from 'node:url';
|
|
4
|
+
import {POLICY_TARGETS, buildPolicyOutputs} from '../lib/policy/render.mjs';
|
|
5
|
+
try {
|
|
6
|
+
const args=process.argv.slice(2); let root=fileURLToPath(new URL('../',import.meta.url)),check=false;
|
|
7
|
+
while(args.length) {const arg=args.shift();if(arg==='--check'&&!check)check=true;else if(arg==='--root'&&args[0]&&!args[0].startsWith('--'))root=path.resolve(args.shift());else throw Error(`Unknown or incomplete option: ${arg}`);}
|
|
8
|
+
const policy=JSON.parse(fs.readFileSync(path.join(root,'lib/policy/role-policy.json'),'utf8'));
|
|
9
|
+
const inputs=new Map(POLICY_TARGETS.map(({path:name})=>[name,fs.readFileSync(path.join(root,name))]));
|
|
10
|
+
// All policy, UTF-8 and marker validation completes before the first write.
|
|
11
|
+
const outputs=buildPolicyOutputs(inputs,policy);
|
|
12
|
+
const changed=[...outputs].filter(([name,bytes])=>!bytes.equals(inputs.get(name)));
|
|
13
|
+
if(check) {if(changed.length){console.error(`Role policy outputs are stale: ${changed.map(([name])=>name).join(', ')}`);process.exitCode=1;}else console.log(`Role policy verified (${outputs.size} targets)`);}
|
|
14
|
+
else {for(const [name,bytes] of changed)fs.writeFileSync(path.join(root,name),bytes);console.log(`Updated role policy (${changed.length} targets)`);}
|
|
15
|
+
} catch(error) {console.error(error.message);process.exitCode=1;}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import {fileURLToPath} from 'node:url';
|
|
2
|
+
import {refreshPackageReferences} from '../lib/installer/package-references.mjs';
|
|
3
|
+
const result=await refreshPackageReferences({packageRoot:fileURLToPath(new URL('../',import.meta.url))});
|
|
4
|
+
console.log(`Updated superpowers references (${result.provenance.revision})`);
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import crypto from 'node:crypto';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
const i = process.argv.indexOf('--root');
|
|
6
|
+
const root = i < 0 ? fileURLToPath(new URL('../', import.meta.url)) : path.resolve(process.argv[i + 1]);
|
|
7
|
+
try {
|
|
8
|
+
const manifest = JSON.parse(fs.readFileSync(path.join(root, 'package.json'), 'utf8'));
|
|
9
|
+
const source = path.join(root, 'node_modules/smol-toml');
|
|
10
|
+
const sourceManifest = JSON.parse(fs.readFileSync(path.join(source, 'package.json'), 'utf8'));
|
|
11
|
+
if (!/^\d+\.\d+\.\d+$/.test(manifest.devDependencies?.['smol-toml'] ?? '') || sourceManifest.version !== manifest.devDependencies['smol-toml']) throw new Error('Vendor source must match the exact smol-toml dev dependency');
|
|
12
|
+
const expected = new Map();
|
|
13
|
+
const sourceFiles = {};
|
|
14
|
+
// This reproducible formatting adjustment changes no executable tokens in the
|
|
15
|
+
// pinned source. Preserve line endings and every non-JS file byte-for-byte.
|
|
16
|
+
const normalization = { javascript: 'strip-trailing-horizontal-whitespace-v1', pattern: '[ \\t]+(?=\\r?$)', flags: 'gm', preserveLineEndings: true };
|
|
17
|
+
for (const name of fs.readdirSync(path.join(source, 'dist')).sort()) if (name.endsWith('.js')) {
|
|
18
|
+
const original = fs.readFileSync(path.join(source, 'dist', name));
|
|
19
|
+
sourceFiles[`dist/${name}`] = crypto.createHash('sha256').update(original).digest('hex');
|
|
20
|
+
const text = new TextDecoder('utf-8', { fatal: true, ignoreBOM: true }).decode(original);
|
|
21
|
+
expected.set(`dist/${name}`, Buffer.from(text.replace(/[ \t]+(?=\r?$)/gm, '')));
|
|
22
|
+
}
|
|
23
|
+
expected.set('LICENSE', fs.readFileSync(path.join(source, 'LICENSE')));
|
|
24
|
+
expected.set('package.json', Buffer.from(JSON.stringify({ name: 'smol-toml', version: sourceManifest.version, type: 'module', license: 'BSD-3-Clause' }, null, 2) + '\n'));
|
|
25
|
+
const hashes = Object.fromEntries([...expected].map(([name, data]) => [name, crypto.createHash('sha256').update(data).digest('hex')]));
|
|
26
|
+
const lockPath = path.join(root, 'package-lock.json');
|
|
27
|
+
const lock = fs.existsSync(lockPath) ? JSON.parse(fs.readFileSync(lockPath)) : undefined;
|
|
28
|
+
expected.set('provenance.json', Buffer.from(JSON.stringify({ name: 'smol-toml', version: sourceManifest.version, normalization, sourceFiles, integrity: lock?.packages?.['node_modules/smol-toml']?.integrity ?? null, files: hashes }, null, 2) + '\n'));
|
|
29
|
+
const destination = path.join(root, 'vendor/toml');
|
|
30
|
+
if (process.argv.includes('--check')) {
|
|
31
|
+
const actual = [];
|
|
32
|
+
function list(dir, relative = '') { if (!fs.existsSync(dir)) return; for (const entry of fs.readdirSync(dir, { withFileTypes: true })) { const name = relative + entry.name; if (entry.isDirectory()) list(path.join(dir, entry.name), `${name}/`); else actual.push(name); } }
|
|
33
|
+
list(destination);
|
|
34
|
+
if (actual.length !== expected.size || actual.some(name => !expected.has(name)) || [...expected].some(([name, bytes]) => !fs.existsSync(path.join(destination, name)) || !fs.readFileSync(path.join(destination, name)).equals(bytes))) throw new Error('Vendored TOML runtime differs from the exact pinned source');
|
|
35
|
+
console.log(`Vendored smol-toml ${sourceManifest.version} verified (${expected.size} files)`);
|
|
36
|
+
} else {
|
|
37
|
+
for (const [name, bytes] of expected) { const file = path.join(destination, name); fs.mkdirSync(path.dirname(file), { recursive: true }); fs.writeFileSync(file, bytes); }
|
|
38
|
+
console.log(`Vendored smol-toml ${sourceManifest.version} (${expected.size} files)`);
|
|
39
|
+
}
|
|
40
|
+
} catch (error) { console.error(error.message); process.exitCode = 1; }
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
Copyright (c) Squirrel Chat et al., All rights reserved.
|
|
2
|
+
|
|
3
|
+
Redistribution and use in source and binary forms, with or without
|
|
4
|
+
modification, are permitted provided that the following conditions are met:
|
|
5
|
+
|
|
6
|
+
1. Redistributions of source code must retain the above copyright notice, this
|
|
7
|
+
list of conditions and the following disclaimer.
|
|
8
|
+
2. Redistributions in binary form must reproduce the above copyright notice,
|
|
9
|
+
this list of conditions and the following disclaimer in the
|
|
10
|
+
documentation and/or other materials provided with the distribution.
|
|
11
|
+
3. Neither the name of the copyright holder nor the names of its contributors
|
|
12
|
+
may be used to endorse or promote products derived from this software without
|
|
13
|
+
specific prior written permission.
|
|
14
|
+
|
|
15
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
|
16
|
+
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
|
17
|
+
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
18
|
+
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
|
19
|
+
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
|
20
|
+
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
|
21
|
+
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
|
22
|
+
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
|
23
|
+
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
24
|
+
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|