@hasna/machines 0.0.21 → 0.0.23

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.
@@ -0,0 +1,405 @@
1
+ #!/usr/bin/env bun
2
+
3
+ import { spawnSync } from "node:child_process";
4
+ import {
5
+ chmodSync,
6
+ cpSync,
7
+ existsSync,
8
+ mkdirSync,
9
+ mkdtempSync,
10
+ rmSync,
11
+ writeFileSync,
12
+ } from "node:fs";
13
+ import { tmpdir } from "node:os";
14
+ import { dirname, join, relative, resolve } from "node:path";
15
+ import { fileURLToPath } from "node:url";
16
+
17
+ const repoRoot = resolve(dirname(fileURLToPath(import.meta.url)), "..");
18
+ const systemPath = "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin";
19
+ const runtimeBinPath = dirname(process.execPath);
20
+ const supportedContractVersion = 1;
21
+
22
+ function parseArgs(argv) {
23
+ const options = {
24
+ json: false,
25
+ keepTemp: false,
26
+ packageDir: process.env.MACHINES_PACKAGE_DIR || repoRoot,
27
+ cliCommand: process.env.MACHINES_CLI_COMMAND || "machines",
28
+ };
29
+ for (let i = 0; i < argv.length; i += 1) {
30
+ const arg = argv[i];
31
+ if (arg === "--json") options.json = true;
32
+ else if (arg === "--keep-temp") options.keepTemp = true;
33
+ else if (arg === "--package-dir") {
34
+ options.packageDir = argv[i + 1];
35
+ i += 1;
36
+ } else if (arg === "--cli-command") {
37
+ options.cliCommand = argv[i + 1];
38
+ i += 1;
39
+ } else if (arg === "--help" || arg === "-h") {
40
+ console.log([
41
+ "Usage: bun scripts/consumer-conformance.mjs [--json] [--package-dir <path>] [--cli-command <command>]",
42
+ "",
43
+ "Verifies downstream app dependency shapes for @hasna/machines:",
44
+ " sdk-local: @hasna/machines/consumer is importable and emits v1 envelopes",
45
+ " future-contract-sdk: fake v2 SDK is detected before route/workspace calls are trusted",
46
+ " global-cli-only: machines CLI JSON can be used when SDK is absent",
47
+ " no-sdk-no-cli: consumer can report graceful unavailable diagnostics",
48
+ ].join("\n"));
49
+ process.exit(0);
50
+ } else {
51
+ throw new Error(`Unknown argument: ${arg}`);
52
+ }
53
+ }
54
+ return options;
55
+ }
56
+
57
+ function run(command, args, options = {}) {
58
+ const result = spawnSync(command, args, {
59
+ encoding: "utf8",
60
+ env: process.env,
61
+ ...options,
62
+ });
63
+ return {
64
+ status: result.status ?? 1,
65
+ stdout: result.stdout || "",
66
+ stderr: result.stderr || "",
67
+ };
68
+ }
69
+
70
+ function shellQuote(value) {
71
+ return `'${value.replace(/'/g, "'\\''")}'`;
72
+ }
73
+
74
+ function resolveCommand(command) {
75
+ if (command.includes("/")) return resolve(command);
76
+ const result = run("bash", ["-lc", `command -v ${shellQuote(command)}`]);
77
+ if (result.status !== 0) throw new Error(`Unable to resolve CLI command: ${command}`);
78
+ return result.stdout.trim().split(/\r?\n/)[0];
79
+ }
80
+
81
+ function packagePath(root, packageName) {
82
+ return join(root, ...packageName.split("/"));
83
+ }
84
+
85
+ function copyPackage(source, target) {
86
+ if (!existsSync(source)) throw new Error(`Package source does not exist: ${source}`);
87
+ const sourceRoot = resolve(source);
88
+ cpSync(source, target, {
89
+ recursive: true,
90
+ filter: (path) => {
91
+ const normalized = relative(sourceRoot, path).replace(/\\/g, "/");
92
+ if (!normalized) return true;
93
+ return normalized !== "node_modules"
94
+ && !normalized.startsWith("node_modules/")
95
+ && normalized !== ".git"
96
+ && !normalized.startsWith(".git/")
97
+ && normalized !== ".hasna"
98
+ && !normalized.startsWith(".hasna/")
99
+ && normalized !== ".takumi"
100
+ && !normalized.startsWith(".takumi/");
101
+ },
102
+ });
103
+ }
104
+
105
+ function createTempApp(name) {
106
+ const appDir = mkdtempSync(join(tmpdir(), `machines-consumer-${name}-`));
107
+ mkdirSync(join(appDir, "node_modules", "@hasna"), { recursive: true });
108
+ return appDir;
109
+ }
110
+
111
+ function installPackage(appDir, sourceDir) {
112
+ const target = packagePath(join(appDir, "node_modules"), "@hasna/machines");
113
+ mkdirSync(dirname(target), { recursive: true });
114
+ copyPackage(sourceDir, target);
115
+ }
116
+
117
+ function installFutureContractPackage(appDir, version = 2) {
118
+ const target = packagePath(join(appDir, "node_modules"), "@hasna/machines");
119
+ mkdirSync(target, { recursive: true });
120
+ writeFileSync(join(target, "package.json"), JSON.stringify({
121
+ name: "@hasna/machines",
122
+ version: `999.0.0-contract-v${version}`,
123
+ type: "module",
124
+ exports: {
125
+ ".": "./consumer.mjs",
126
+ "./consumer": "./consumer.mjs",
127
+ },
128
+ }, null, 2));
129
+ writeFileSync(join(target, "consumer.mjs"), `
130
+ export const MACHINES_CONSUMER_CONTRACT_VERSION = ${version};
131
+ export const MACHINES_CONSUMER_CONTRACT = {
132
+ schema_version: ${version},
133
+ package_name: '@hasna/machines',
134
+ entrypoint: '@hasna/machines/consumer',
135
+ capabilities: {
136
+ topology: true,
137
+ compatibility: true,
138
+ route_resolution: true,
139
+ cli_json_fallback: true,
140
+ workspace_path_mapping: true,
141
+ workspace_diagnostics: true,
142
+ },
143
+ envelopes: ['topology', 'route', 'workspace', 'compatibility'],
144
+ stable_exports: ['resolveMachineRoute', 'resolveMachineWorkspace', 'checkMachineCompatibility'],
145
+ };
146
+ export function resolveMachineRoute() {
147
+ throw new Error('future route resolver should not be called by guarded consumers');
148
+ }
149
+ export function resolveMachineWorkspace() {
150
+ throw new Error('future workspace resolver should not be called by guarded consumers');
151
+ }
152
+ export function checkMachineCompatibility() {
153
+ throw new Error('future compatibility resolver should not be called by guarded consumers');
154
+ }
155
+ `);
156
+ }
157
+
158
+ function writeSdkProbe(appDir) {
159
+ const script = join(appDir, "sdk-probe.mjs");
160
+ writeFileSync(script, `
161
+ import {
162
+ MACHINES_CONSUMER_CONTRACT,
163
+ MACHINES_CONSUMER_CONTRACT_VERSION,
164
+ checkMachineCompatibility,
165
+ discoverMachineTopology,
166
+ resolveMachineRoute,
167
+ resolveMachineWorkspace,
168
+ } from '@hasna/machines/consumer';
169
+
170
+ const topology = discoverMachineTopology({ includeTailscale: false, now: new Date('2026-06-09T00:00:00.000Z') });
171
+ const route = resolveMachineRoute('local', { topology, now: new Date('2026-06-09T00:00:00.000Z') });
172
+ const workspace = resolveMachineWorkspace({
173
+ machineId: 'local',
174
+ projectId: 'open-knowledge',
175
+ repoName: 'open-knowledge',
176
+ workspaceRoot: '/tmp/workspace',
177
+ projectRoot: '/tmp/workspace/open-knowledge',
178
+ openFilesRoot: '/tmp/workspace/open-files',
179
+ topology,
180
+ now: new Date('2026-06-09T00:00:00.000Z'),
181
+ });
182
+ const compatibility = checkMachineCompatibility({
183
+ machineId: 'local',
184
+ commands: [],
185
+ packages: [],
186
+ workspaces: [],
187
+ now: new Date('2026-06-09T00:00:00.000Z'),
188
+ });
189
+
190
+ console.log(JSON.stringify({
191
+ source: 'sdk',
192
+ supported: MACHINES_CONSUMER_CONTRACT_VERSION <= ${supportedContractVersion},
193
+ contract_version: MACHINES_CONSUMER_CONTRACT_VERSION,
194
+ entrypoint: MACHINES_CONSUMER_CONTRACT.entrypoint,
195
+ envelopes: MACHINES_CONSUMER_CONTRACT.envelopes,
196
+ capabilities: MACHINES_CONSUMER_CONTRACT.capabilities,
197
+ topology: { schema_version: topology.schema_version, machines: topology.machines.length },
198
+ route: { schema_version: route.schema_version, ok: route.ok, route: route.route, target: route.target },
199
+ workspace: { schema_version: workspace.schema_version, ok: workspace.ok, project_root: workspace.paths.project_root.path },
200
+ compatibility: { schema_version: compatibility.schema_version, ok: compatibility.ok },
201
+ }));
202
+ `);
203
+ return script;
204
+ }
205
+
206
+ function writeFutureProbe(appDir) {
207
+ const script = join(appDir, "future-probe.mjs");
208
+ writeFileSync(script, `
209
+ import {
210
+ MACHINES_CONSUMER_CONTRACT,
211
+ MACHINES_CONSUMER_CONTRACT_VERSION,
212
+ } from '@hasna/machines/consumer';
213
+
214
+ const supported = MACHINES_CONSUMER_CONTRACT_VERSION <= ${supportedContractVersion};
215
+ console.log(JSON.stringify({
216
+ source: 'sdk',
217
+ supported,
218
+ contract_version: MACHINES_CONSUMER_CONTRACT_VERSION,
219
+ entrypoint: MACHINES_CONSUMER_CONTRACT.entrypoint,
220
+ error: supported ? null : 'unsupported_contract_version:' + MACHINES_CONSUMER_CONTRACT_VERSION,
221
+ trusted_envelopes: supported ? MACHINES_CONSUMER_CONTRACT.envelopes : [],
222
+ }));
223
+ `);
224
+ return script;
225
+ }
226
+
227
+ function writeCliProbe(appDir) {
228
+ const script = join(appDir, "cli-probe.mjs");
229
+ writeFileSync(script, `
230
+ import { spawnSync } from 'node:child_process';
231
+
232
+ function run(args) {
233
+ const result = spawnSync('machines', args, { encoding: 'utf8', env: process.env });
234
+ if ((result.status ?? 1) !== 0) throw new Error(result.stderr || 'machines command failed');
235
+ return JSON.parse(result.stdout);
236
+ }
237
+
238
+ const topology = run(['topology', '--no-tailscale', '--json']);
239
+ const route = run(['route', '--machine', 'local', '--no-tailscale', '--json']);
240
+ console.log(JSON.stringify({
241
+ source: 'cli',
242
+ supported: true,
243
+ topology: { schema_version: topology.schema_version, machines: topology.machines.length },
244
+ route: { schema_version: route.schema_version, ok: route.ok, route: route.route, target: route.target },
245
+ }));
246
+ `);
247
+ return script;
248
+ }
249
+
250
+ function writeUnavailableProbe(appDir) {
251
+ const script = join(appDir, "unavailable-probe.mjs");
252
+ writeFileSync(script, `
253
+ import { spawnSync } from 'node:child_process';
254
+
255
+ let sdk = false;
256
+ try {
257
+ await import('@hasna/machines/consumer');
258
+ sdk = true;
259
+ } catch {
260
+ sdk = false;
261
+ }
262
+ const cli = spawnSync('bash', ['-lc', 'command -v machines >/dev/null 2>&1'], {
263
+ encoding: 'utf8',
264
+ env: process.env,
265
+ }).status === 0;
266
+ console.log(JSON.stringify({
267
+ source: 'unavailable',
268
+ supported: false,
269
+ sdk_available: sdk,
270
+ cli_available: cli,
271
+ error: sdk || cli ? null : 'machines_unavailable',
272
+ }));
273
+ `);
274
+ return script;
275
+ }
276
+
277
+ function writeMachinesWrapper(binDir, command) {
278
+ mkdirSync(binDir, { recursive: true });
279
+ const wrapper = join(binDir, "machines");
280
+ writeFileSync(wrapper, `#!/bin/sh\nexec ${JSON.stringify(command)} "$@"\n`);
281
+ chmodSync(wrapper, 0o755);
282
+ return wrapper;
283
+ }
284
+
285
+ function runNodeScript(script, appDir, env) {
286
+ const result = run(process.execPath, [script], {
287
+ cwd: appDir,
288
+ env,
289
+ });
290
+ if (result.status !== 0) {
291
+ throw new Error(`${script} failed\nstdout:\n${result.stdout}\nstderr:\n${result.stderr}`);
292
+ }
293
+ return JSON.parse(result.stdout);
294
+ }
295
+
296
+ function assertCase(name, output) {
297
+ if (name === "sdk-local") {
298
+ if (output.source !== "sdk" || output.contract_version !== 1 || output.supported !== true) {
299
+ throw new Error(`${name}: invalid SDK output\n${JSON.stringify(output, null, 2)}`);
300
+ }
301
+ for (const envelope of ["topology", "route", "workspace", "compatibility"]) {
302
+ if (!output.envelopes.includes(envelope)) throw new Error(`${name}: missing envelope ${envelope}`);
303
+ if (output[envelope].schema_version !== 1) throw new Error(`${name}: ${envelope} schema mismatch`);
304
+ }
305
+ }
306
+ if (name === "future-contract-sdk") {
307
+ if (output.supported !== false || output.error !== "unsupported_contract_version:2" || output.trusted_envelopes.length !== 0) {
308
+ throw new Error(`${name}: future contract was not rejected\n${JSON.stringify(output, null, 2)}`);
309
+ }
310
+ }
311
+ if (name === "global-cli-only") {
312
+ if (output.source !== "cli" || output.supported !== true || output.topology.schema_version !== 1 || output.route.schema_version !== 1) {
313
+ throw new Error(`${name}: invalid CLI output\n${JSON.stringify(output, null, 2)}`);
314
+ }
315
+ }
316
+ if (name === "no-sdk-no-cli") {
317
+ if (output.sdk_available !== false || output.cli_available !== false || output.error !== "machines_unavailable") {
318
+ throw new Error(`${name}: unavailable case did not degrade cleanly\n${JSON.stringify(output, null, 2)}`);
319
+ }
320
+ }
321
+ }
322
+
323
+ function runCase(input) {
324
+ const appDir = createTempApp(input.name);
325
+ try {
326
+ if (input.installSdk) installPackage(appDir, input.packageDir);
327
+ if (input.installFutureSdk) installFutureContractPackage(appDir, 2);
328
+ const script = input.writeProbe(appDir);
329
+ const env = {
330
+ ...process.env,
331
+ PATH: input.path,
332
+ HASNA_MACHINES_DB_PATH: join(appDir, "machines.db"),
333
+ HASNA_MACHINES_MANIFEST_PATH: join(appDir, "machines.json"),
334
+ HASNA_MACHINES_MACHINE_ID: "consumer-conformance-local",
335
+ };
336
+ const output = runNodeScript(script, appDir, env);
337
+ assertCase(input.name, output);
338
+ return {
339
+ name: input.name,
340
+ ok: true,
341
+ app_dir: input.keepTemp ? appDir : null,
342
+ output,
343
+ };
344
+ } finally {
345
+ if (!input.keepTemp) rmSync(appDir, { recursive: true, force: true });
346
+ }
347
+ }
348
+
349
+ function main() {
350
+ const options = parseArgs(process.argv.slice(2));
351
+ const packageDir = resolve(options.packageDir);
352
+ const cliCommand = resolveCommand(options.cliCommand);
353
+ const cliBinDir = mkdtempSync(join(tmpdir(), "machines-consumer-cli-bin-"));
354
+ const emptyPathDir = mkdtempSync(join(tmpdir(), "machines-consumer-empty-path-"));
355
+ writeMachinesWrapper(cliBinDir, cliCommand);
356
+ try {
357
+ const cases = [
358
+ {
359
+ name: "sdk-local",
360
+ installSdk: true,
361
+ path: systemPath,
362
+ writeProbe: writeSdkProbe,
363
+ },
364
+ {
365
+ name: "future-contract-sdk",
366
+ installFutureSdk: true,
367
+ path: systemPath,
368
+ writeProbe: writeFutureProbe,
369
+ },
370
+ {
371
+ name: "global-cli-only",
372
+ path: `${cliBinDir}:${runtimeBinPath}:${systemPath}`,
373
+ writeProbe: writeCliProbe,
374
+ },
375
+ {
376
+ name: "no-sdk-no-cli",
377
+ path: emptyPathDir,
378
+ writeProbe: writeUnavailableProbe,
379
+ },
380
+ ].map((entry) => ({
381
+ ...entry,
382
+ packageDir,
383
+ keepTemp: options.keepTemp,
384
+ }));
385
+
386
+ const results = cases.map(runCase);
387
+ const summary = {
388
+ ok: true,
389
+ package_dir: packageDir,
390
+ cli_command: cliCommand,
391
+ supported_contract_version: supportedContractVersion,
392
+ cases: results,
393
+ };
394
+ if (options.json) console.log(JSON.stringify(summary, null, 2));
395
+ else {
396
+ console.log("machines consumer conformance: ok");
397
+ for (const result of results) console.log(`- ${result.name}: ${result.output.source}/${result.output.supported ? "supported" : result.output.error}`);
398
+ }
399
+ } finally {
400
+ if (!options.keepTemp) rmSync(cliBinDir, { recursive: true, force: true });
401
+ if (!options.keepTemp) rmSync(emptyPathDir, { recursive: true, force: true });
402
+ }
403
+ }
404
+
405
+ main();