This repository was archived by the owner on Jul 24, 2026. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathflake.nix
More file actions
203 lines (179 loc) · 8.02 KB
/
Copy pathflake.nix
File metadata and controls
203 lines (179 loc) · 8.02 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
{
description = "smalltalk - file-folder coordination CLI and TypeScript API";
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
flake-utils.url = "github:numtide/flake-utils";
pty.url = "github:compoundingtech/pty";
pty.inputs.nixpkgs.follows = "nixpkgs";
};
outputs =
{
self,
nixpkgs,
flake-utils,
pty,
}:
flake-utils.lib.eachDefaultSystem (
system:
let
pkgs = import nixpkgs { inherit system; };
# package.json is the single source of truth for the version, so a
# release bump does not need a matching edit here. Reconciling this
# with a richer build identity (git rev, dirty marker) is issue #103.
version = (builtins.fromJSON (builtins.readFile ./package.json)).version;
# npm names of local `file:` deps that resolve to sibling repos, mapped
# to the store path holding their package root. buildNpmPackage cannot
# fetch these, so they are linked into node_modules after the copy.
# The key must match package.json / package-lock.json exactly, since
# that is the specifier Node resolves.
siblingLinks = {
"@compoundingtech/pty" = "${pty.packages.${system}.default}/lib/pty";
};
# The shells `st completions <shell>` can generate. The generator emits
# `complete -c st` etc. unconditionally, so every script is for `st`
# regardless of which bin name invoked it.
completionShells = [
"bash"
"zsh"
"fish"
];
smalltalk = pkgs.buildNpmPackage {
pname = "smalltalk";
inherit version;
src = self;
# TODO(rust): a Rust rewrite drops npm entirely, and this hash with it.
# Regenerate with: nix run nixpkgs#prefetch-npm-deps -- package-lock.json
npmDepsHash = "sha256-FsAjO/CYxyMf30F8ESsxFDI30ckYv1uS4+nkvn9Fl+k=";
nodejs = pkgs.nodejs_24;
npmInstallFlags = [ "--omit=dev" ];
# Sources ship as TypeScript and run through node's type stripping;
# there is no compile step to produce a dist/.
dontNpmBuild = true;
nativeBuildInputs = [ pkgs.installShellFiles ];
installPhase = ''
runHook preInstall
mkdir -p $out/lib/smalltalk
cp -r . $out/lib/smalltalk
# TODO(rust): sibling linking exists only because npm cannot resolve
# `file:` deps in a sandbox; a Cargo build has no equivalent.
${pkgs.lib.concatLines (
pkgs.lib.mapAttrsToList (npmName: storePath: ''
mkdir -p "$out/lib/smalltalk/node_modules/$(dirname ${npmName})"
ln -s ${storePath} "$out/lib/smalltalk/node_modules/${npmName}"
'') siblingLinks
)}
# package.json declares `st` and `smalltalk`; both reach the same
# cli.ts. `_ST_INVOKED_AS` is what bin/st exports so help banners and
# the MCP server report the name the user actually typed.
# TODO(rust): a compiled binary replaces these strip-types shims.
mkdir -p $out/bin
for _binName in st smalltalk; do
cat > $out/bin/$_binName <<EOF
#!${pkgs.runtimeShell}
export _ST_INVOKED_AS=$_binName
exec ${pkgs.nodejs_24}/bin/node --experimental-strip-types \\
$out/lib/smalltalk/src/cli.ts "\$@"
EOF
chmod +x $out/bin/$_binName
done
runHook postInstall
'';
# Completions are generated by the binary we just built rather than
# committed, so they cannot drift from the command tree in
# src/commands/completions.ts. The CLI touches $HOME on startup, which
# is not writable in the sandbox.
postInstall = ''
export HOME=$(mktemp -d)
${pkgs.lib.concatMapStringsSep "\n" (shell: ''
$out/bin/st completions ${shell} > completions-${shell}
'') completionShells}
installShellCompletion --cmd st \
--bash completions-bash \
--zsh completions-zsh \
--fish completions-fish
'';
meta = {
description = "File-folder coordination CLI and embeddable TypeScript API";
homepage = "https://github.com/compoundingtech/smalltalk";
license = pkgs.lib.licenses.mit;
mainProgram = "st";
};
};
# Base for checks that need devDependencies (tsc, vitest), which the
# shipped package deliberately omits.
#
# Two things make this awkward enough to be worth explaining. First,
# npm cannot resolve the `file:../pty` devDependency from a store
# symlink: it chmods the linked `bin/pty`, which is read-only in the
# store, and fails EPERM. So the sibling is *copied* to the exact
# relative path the lockfile names and made writable. Second,
# `--ignore-scripts` keeps node-pty's native build (`hasInstallScript`)
# out of the sandbox; nothing gated here needs the native addon.
#
# These run the check in the build dir and `touch $out` rather than
# installing anything, so the store-external `../pty` symlink npm
# leaves in node_modules never has to survive into the output.
mkDevCheck =
name: script:
smalltalk.overrideAttrs (old: {
pname = "smalltalk-${name}";
npmInstallFlags = [ ];
npmFlags = [ "--ignore-scripts" ];
postPatch = ''
cp -r ${pty.packages.${system}.default}/lib/pty ../pty
chmod -R u+w ../pty
'';
installPhase = ''
runHook preInstall
export HOME=$(mktemp -d)
${script}
touch $out
runHook postInstall
'';
postInstall = "";
# Nothing is installed, so the fixup hooks (patchelf,
# noBrokenSymlinks) have nothing useful to do.
dontFixup = true;
});
in
{
packages.smalltalk = smalltalk;
packages.default = smalltalk;
checks.help = pkgs.runCommand "smalltalk-help-${version}" { } ''
export HOME=$(mktemp -d)
${smalltalk}/bin/st --help > /dev/null
touch $out
'';
# Guards the completions contract: every installed shell still gets a
# non-empty script, and fish in particular still binds to `st` (the name
# the installed st.fish file claims).
checks.completions = pkgs.runCommand "smalltalk-completions-${version}" { } ''
export HOME=$(mktemp -d)
${pkgs.lib.concatMapStringsSep "\n" (shell: ''
${smalltalk}/bin/st completions ${shell} | grep -q . \
|| { echo "empty ${shell} completions" >&2; exit 1; }
'') completionShells}
${smalltalk}/bin/st completions fish | grep -q '^complete -c st ' \
|| { echo "fish completions do not bind to \`st\`" >&2; exit 1; }
touch $out
'';
# `npm run build` is typecheck-only (both tsconfigs set `noEmit`), so
# this gates src/ and examples/ against the compiler.
checks.typecheck = mkDevCheck "typecheck" "npm run build";
# The vitest `unit` project is NOT gated here yet. It very nearly works
# (1322/1323 pass in the sandbox), but tests/unit/cli.test.ts asserts
# `st <semver>+<short-sha>`, and the SHA comes from shelling out to
# `git rev-parse` in the source dir — which a hermetic build has no
# `.git` for. That is the build-identity question tracked in #103, not
# something to paper over here. The `integration` project is a separate
# matter: it shells out to rsync and spawns real pty sessions, which the
# build sandbox has no business hosting.
devShells.default = pkgs.mkShell {
packages = [
pkgs.nodejs_24
smalltalk
];
};
}
);
}