@aikdna/kdna-studio-cli 0.1.0 → 0.1.2
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/package.json +6 -3
- package/tests/cli.test.js +166 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aikdna/kdna-studio-cli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"description": "Official KDNA Studio command-line authoring entry for creating, locking, compiling, and exporting trusted .kdna assets.",
|
|
5
5
|
"type": "commonjs",
|
|
6
6
|
"bin": {
|
|
@@ -8,10 +8,13 @@
|
|
|
8
8
|
},
|
|
9
9
|
"files": [
|
|
10
10
|
"bin/",
|
|
11
|
+
"tests/",
|
|
11
12
|
"README.md"
|
|
12
13
|
],
|
|
13
14
|
"scripts": {
|
|
14
|
-
"
|
|
15
|
+
"lint": "node --check bin/kdna-studio.js && node --check tests/cli.test.js",
|
|
16
|
+
"test": "node --test tests/*.test.js",
|
|
17
|
+
"test:all": "npm run lint && npm test"
|
|
15
18
|
},
|
|
16
19
|
"keywords": [
|
|
17
20
|
"kdna",
|
|
@@ -34,6 +37,6 @@
|
|
|
34
37
|
"node": ">=18"
|
|
35
38
|
},
|
|
36
39
|
"dependencies": {
|
|
37
|
-
"@aikdna/kdna-studio-core": "^1.3.
|
|
40
|
+
"@aikdna/kdna-studio-core": "^1.3.2"
|
|
38
41
|
}
|
|
39
42
|
}
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
const assert = require('node:assert/strict');
|
|
2
|
+
const fs = require('node:fs');
|
|
3
|
+
const os = require('node:os');
|
|
4
|
+
const path = require('node:path');
|
|
5
|
+
const { spawnSync } = require('node:child_process');
|
|
6
|
+
const test = require('node:test');
|
|
7
|
+
|
|
8
|
+
const root = path.resolve(__dirname, '..');
|
|
9
|
+
const cli = path.join(root, 'bin', 'kdna-studio.js');
|
|
10
|
+
|
|
11
|
+
function run(args, options = {}) {
|
|
12
|
+
return spawnSync(process.execPath, [cli, ...args], {
|
|
13
|
+
cwd: options.cwd || root,
|
|
14
|
+
env: {
|
|
15
|
+
...process.env,
|
|
16
|
+
KDNA_IDENTITY_DIR: path.join(options.tmp || os.tmpdir(), 'no-identity'),
|
|
17
|
+
},
|
|
18
|
+
encoding: 'utf8',
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function tmpDir() {
|
|
23
|
+
return fs.mkdtempSync(path.join(os.tmpdir(), 'kdna-studio-cli-'));
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function createLockedProject(t) {
|
|
27
|
+
const tmp = tmpDir();
|
|
28
|
+
t.after(() => fs.rmSync(tmp, { recursive: true, force: true }));
|
|
29
|
+
const projectDir = path.join(tmp, 'project');
|
|
30
|
+
let result = run(['create', projectDir, '--name', '@test/example'], { tmp });
|
|
31
|
+
assert.equal(result.status, 0, result.stderr);
|
|
32
|
+
|
|
33
|
+
const evidencePath = path.join(tmp, 'evidence.md');
|
|
34
|
+
fs.writeFileSync(evidencePath, 'Evidence that specific claims should outrank broad claims.');
|
|
35
|
+
result = run(['import', projectDir, evidencePath], { tmp });
|
|
36
|
+
assert.equal(result.status, 0, result.stderr);
|
|
37
|
+
|
|
38
|
+
result = run([
|
|
39
|
+
'card',
|
|
40
|
+
'add',
|
|
41
|
+
projectDir,
|
|
42
|
+
'axiom',
|
|
43
|
+
'--field',
|
|
44
|
+
'one_sentence=Prefer specific evidence over broad claims',
|
|
45
|
+
'--field',
|
|
46
|
+
'applies_when=["reviewing content"]',
|
|
47
|
+
'--field',
|
|
48
|
+
'does_not_apply_when=["pure formatting"]',
|
|
49
|
+
'--field',
|
|
50
|
+
'failure_risk=generic advice',
|
|
51
|
+
], { tmp });
|
|
52
|
+
assert.equal(result.status, 0, result.stderr);
|
|
53
|
+
const cardId = result.stdout.match(/Added card: (\S+)/)?.[1];
|
|
54
|
+
assert.ok(cardId, result.stdout);
|
|
55
|
+
|
|
56
|
+
result = run([
|
|
57
|
+
'card',
|
|
58
|
+
'approve',
|
|
59
|
+
projectDir,
|
|
60
|
+
cardId,
|
|
61
|
+
'--by',
|
|
62
|
+
'expert',
|
|
63
|
+
'--statement',
|
|
64
|
+
'I confirm this judgment.',
|
|
65
|
+
], { tmp });
|
|
66
|
+
assert.equal(result.status, 0, result.stderr);
|
|
67
|
+
|
|
68
|
+
return { tmp, projectDir, cardId };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
test('prints help', () => {
|
|
72
|
+
const result = run(['--help']);
|
|
73
|
+
assert.equal(result.status, 0);
|
|
74
|
+
assert.match(result.stdout, /kdna-studio/);
|
|
75
|
+
assert.match(result.stdout, /export <project>/);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
test('rejects unknown commands with input error', () => {
|
|
79
|
+
const result = run(['unknown']);
|
|
80
|
+
assert.equal(result.status, 2);
|
|
81
|
+
assert.match(result.stderr, /Unknown command/);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
test('creates a Studio project and rejects accidental overwrite', (t) => {
|
|
85
|
+
const tmp = tmpDir();
|
|
86
|
+
t.after(() => fs.rmSync(tmp, { recursive: true, force: true }));
|
|
87
|
+
const projectDir = path.join(tmp, 'project');
|
|
88
|
+
|
|
89
|
+
let result = run(['create', projectDir, '--name', '@test/example'], { tmp });
|
|
90
|
+
assert.equal(result.status, 0, result.stderr);
|
|
91
|
+
const projectPath = path.join(projectDir, 'studio.project.json');
|
|
92
|
+
assert.ok(fs.existsSync(projectPath));
|
|
93
|
+
const project = JSON.parse(fs.readFileSync(projectPath, 'utf8'));
|
|
94
|
+
assert.equal(project.name, '@test/example');
|
|
95
|
+
|
|
96
|
+
result = run(['create', projectDir, '--name', '@test/example'], { tmp });
|
|
97
|
+
assert.equal(result.status, 2);
|
|
98
|
+
assert.match(result.stderr, /Directory already exists/);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
test('blocks compile and export until judgment cards are Human Locked', (t) => {
|
|
102
|
+
const tmp = tmpDir();
|
|
103
|
+
t.after(() => fs.rmSync(tmp, { recursive: true, force: true }));
|
|
104
|
+
const projectDir = path.join(tmp, 'project');
|
|
105
|
+
let result = run(['create', projectDir, '--name', '@test/example'], { tmp });
|
|
106
|
+
assert.equal(result.status, 0, result.stderr);
|
|
107
|
+
|
|
108
|
+
result = run([
|
|
109
|
+
'card',
|
|
110
|
+
'add',
|
|
111
|
+
projectDir,
|
|
112
|
+
'axiom',
|
|
113
|
+
'--field',
|
|
114
|
+
'one_sentence=Prefer specific evidence over broad claims',
|
|
115
|
+
], { tmp });
|
|
116
|
+
assert.equal(result.status, 0, result.stderr);
|
|
117
|
+
|
|
118
|
+
result = run(['lock', projectDir], { tmp });
|
|
119
|
+
assert.equal(result.status, 4);
|
|
120
|
+
assert.match(result.stderr, /Human Lock Gate blocked export/);
|
|
121
|
+
|
|
122
|
+
result = run(['compile', projectDir, '--out', path.join(tmp, 'build')], { tmp });
|
|
123
|
+
assert.equal(result.status, 4);
|
|
124
|
+
assert.match(result.stderr, /Human Lock Gate blocked compile/);
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
test('runs the trusted authoring workflow through compile and export', (t) => {
|
|
128
|
+
const { tmp, projectDir, cardId } = createLockedProject(t);
|
|
129
|
+
|
|
130
|
+
let result = run(['card', 'list', projectDir], { tmp });
|
|
131
|
+
assert.equal(result.status, 0, result.stderr);
|
|
132
|
+
assert.match(result.stdout, new RegExp(`${cardId}\\s+axiom\\s+locked\\s+locked`));
|
|
133
|
+
|
|
134
|
+
result = run(['lock', projectDir], { tmp });
|
|
135
|
+
assert.equal(result.status, 0, result.stderr);
|
|
136
|
+
assert.match(result.stdout, /Human Lock Gate passed/);
|
|
137
|
+
|
|
138
|
+
const buildDir = path.join(tmp, 'build');
|
|
139
|
+
result = run(['compile', projectDir, '--out', buildDir], { tmp });
|
|
140
|
+
assert.equal(result.status, 0, result.stderr);
|
|
141
|
+
assert.ok(fs.existsSync(path.join(buildDir, 'kdna.json')));
|
|
142
|
+
assert.ok(fs.existsSync(path.join(buildDir, 'KDNA_CARD.json')));
|
|
143
|
+
assert.ok(fs.existsSync(path.join(buildDir, 'reports', 'provenance-report.json')));
|
|
144
|
+
const manifest = JSON.parse(fs.readFileSync(path.join(buildDir, 'kdna.json'), 'utf8'));
|
|
145
|
+
assert.equal(manifest.authoring.compiler, '@aikdna/kdna-studio-core');
|
|
146
|
+
assert.equal(manifest.authoring.human_confirmed, true);
|
|
147
|
+
assert.ok(manifest.authoring.human_lock_count >= 1);
|
|
148
|
+
|
|
149
|
+
const outFile = path.join(tmp, 'dist', 'example.kdna');
|
|
150
|
+
result = run(['export', projectDir, '--out', outFile], { tmp });
|
|
151
|
+
assert.equal(result.status, 0, result.stderr);
|
|
152
|
+
assert.ok(fs.existsSync(outFile));
|
|
153
|
+
const zipMagic = fs.readFileSync(outFile).subarray(0, 4).toString('hex');
|
|
154
|
+
assert.equal(zipMagic, '504b0304');
|
|
155
|
+
const receipt = JSON.parse(fs.readFileSync(path.join(tmp, 'dist', 'build-receipt.json'), 'utf8'));
|
|
156
|
+
assert.equal(receipt.signature_status, 'unsigned');
|
|
157
|
+
assert.match(receipt.asset_digest, /^sha256:/);
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
test('refuses signing without runtime identity keys', (t) => {
|
|
161
|
+
const { tmp, projectDir } = createLockedProject(t);
|
|
162
|
+
const outFile = path.join(tmp, 'dist', 'signed.kdna');
|
|
163
|
+
const result = run(['export', projectDir, '--out', outFile, '--sign'], { tmp });
|
|
164
|
+
assert.equal(result.status, 5);
|
|
165
|
+
assert.match(result.stderr, /Signing requires KDNA identity keys/);
|
|
166
|
+
});
|