@kntic/kntic 0.10.0 → 0.11.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/package.json +1 -1
- package/src/cli.js +4 -1
- package/src/commands/start.js +57 -14
- package/src/commands/start.test.js +171 -29
- package/src/commands/update.js +41 -27
- package/src/commands/update.test.js +145 -1
- package/src/commands/usage.js +6 -5
package/package.json
CHANGED
package/src/cli.js
CHANGED
|
@@ -17,7 +17,10 @@ if (!subcommand || subcommand === "usage") {
|
|
|
17
17
|
});
|
|
18
18
|
} else if (subcommand === "start") {
|
|
19
19
|
try {
|
|
20
|
-
const startOpts = {
|
|
20
|
+
const startOpts = {
|
|
21
|
+
screen: args.includes("--screen"),
|
|
22
|
+
tmux: args.includes("--tmux"),
|
|
23
|
+
};
|
|
21
24
|
commands.start(startOpts);
|
|
22
25
|
} catch (err) {
|
|
23
26
|
console.error(`Error: ${err.message}`);
|
package/src/commands/start.js
CHANGED
|
@@ -14,11 +14,24 @@ function isScreenAvailable() {
|
|
|
14
14
|
}
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
+
function isTmuxAvailable() {
|
|
18
|
+
try {
|
|
19
|
+
execSync("which tmux", { stdio: "ignore" });
|
|
20
|
+
return true;
|
|
21
|
+
} catch {
|
|
22
|
+
return false;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
17
26
|
function isInsideScreen() {
|
|
18
27
|
return !!process.env.STY;
|
|
19
28
|
}
|
|
20
29
|
|
|
21
|
-
function
|
|
30
|
+
function isInsideTmux() {
|
|
31
|
+
return !!process.env.TMUX;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function getSessionName() {
|
|
22
35
|
try {
|
|
23
36
|
const content = readFileSync(".kntic.env", "utf8");
|
|
24
37
|
const match = content.match(/^KNTIC_PRJ_PREFIX=(.+)$/m);
|
|
@@ -31,24 +44,54 @@ function getScreenName() {
|
|
|
31
44
|
return basename(process.cwd());
|
|
32
45
|
}
|
|
33
46
|
|
|
47
|
+
function shellQuote(value) {
|
|
48
|
+
return `'${String(value).replace(/'/g, `'\\''`)}'`;
|
|
49
|
+
}
|
|
50
|
+
|
|
34
51
|
function start(options = {}) {
|
|
52
|
+
const useScreen = !!options.screen;
|
|
53
|
+
const useTmux = !!options.tmux;
|
|
54
|
+
|
|
55
|
+
if (useScreen && useTmux) {
|
|
56
|
+
throw new Error("--screen and --tmux cannot be used together");
|
|
57
|
+
}
|
|
58
|
+
|
|
35
59
|
const provider = getComposeProvider();
|
|
36
60
|
const composeCmd = `${provider} -f kntic.yml --env-file .kntic.env up --build`;
|
|
37
|
-
const useScreen = !!options.screen;
|
|
38
61
|
|
|
39
62
|
if (useScreen && isScreenAvailable() && !isInsideScreen()) {
|
|
40
|
-
const
|
|
41
|
-
console.log(`Starting KNTIC services in screen session "${
|
|
42
|
-
execSync(`screen -S ${
|
|
43
|
-
|
|
44
|
-
if (useScreen && isInsideScreen()) {
|
|
45
|
-
console.log("Already inside a screen session, skipping screen wrapper.");
|
|
46
|
-
} else if (useScreen && !isScreenAvailable()) {
|
|
47
|
-
console.log("screen is not available, starting without screen wrapper.");
|
|
48
|
-
}
|
|
49
|
-
console.log("Starting KNTIC services…");
|
|
50
|
-
execSync(composeCmd, { stdio: "inherit" });
|
|
63
|
+
const sessionName = getSessionName();
|
|
64
|
+
console.log(`Starting KNTIC services in screen session "${sessionName}"…`);
|
|
65
|
+
execSync(`screen -S ${shellQuote(sessionName)} ${composeCmd}`, { stdio: "inherit" });
|
|
66
|
+
return;
|
|
51
67
|
}
|
|
68
|
+
|
|
69
|
+
if (useTmux && isTmuxAvailable() && !isInsideTmux()) {
|
|
70
|
+
const sessionName = getSessionName();
|
|
71
|
+
console.log(`Starting KNTIC services in tmux session "${sessionName}"…`);
|
|
72
|
+
execSync(`tmux new-session -s ${shellQuote(sessionName)} ${shellQuote(composeCmd)}`, { stdio: "inherit" });
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (useScreen && isInsideScreen()) {
|
|
77
|
+
console.log("Already inside a screen session, skipping screen wrapper.");
|
|
78
|
+
} else if (useScreen && !isScreenAvailable()) {
|
|
79
|
+
console.log("screen is not available, starting without screen wrapper.");
|
|
80
|
+
} else if (useTmux && isInsideTmux()) {
|
|
81
|
+
console.log("Already inside a tmux session, skipping tmux wrapper.");
|
|
82
|
+
} else if (useTmux && !isTmuxAvailable()) {
|
|
83
|
+
console.log("tmux is not available, starting without tmux wrapper.");
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
console.log("Starting KNTIC services…");
|
|
87
|
+
execSync(composeCmd, { stdio: "inherit" });
|
|
52
88
|
}
|
|
53
89
|
|
|
54
|
-
module.exports = start
|
|
90
|
+
module.exports = Object.assign(start, {
|
|
91
|
+
isScreenAvailable,
|
|
92
|
+
isTmuxAvailable,
|
|
93
|
+
isInsideScreen,
|
|
94
|
+
isInsideTmux,
|
|
95
|
+
getSessionName,
|
|
96
|
+
shellQuote,
|
|
97
|
+
});
|
|
@@ -5,18 +5,38 @@ const assert = require("node:assert/strict");
|
|
|
5
5
|
const fs = require("fs");
|
|
6
6
|
const path = require("path");
|
|
7
7
|
const os = require("os");
|
|
8
|
+
const childProcess = require("child_process");
|
|
8
9
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
10
|
+
function withMockedExecSync(mockImpl, run) {
|
|
11
|
+
const originalExecSync = childProcess.execSync;
|
|
12
|
+
childProcess.execSync = mockImpl;
|
|
13
|
+
delete require.cache[require.resolve("./start")];
|
|
14
|
+
|
|
15
|
+
try {
|
|
16
|
+
return run(require("./start"));
|
|
17
|
+
} finally {
|
|
18
|
+
childProcess.execSync = originalExecSync;
|
|
19
|
+
delete require.cache[require.resolve("./start")];
|
|
20
|
+
}
|
|
16
21
|
}
|
|
17
22
|
|
|
18
|
-
|
|
19
|
-
|
|
23
|
+
function captureConsoleLogs(run) {
|
|
24
|
+
const originalLog = console.log;
|
|
25
|
+
const logs = [];
|
|
26
|
+
console.log = (...args) => logs.push(args.join(" "));
|
|
27
|
+
|
|
28
|
+
try {
|
|
29
|
+
run(logs);
|
|
30
|
+
} finally {
|
|
31
|
+
console.log = originalLog;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return logs;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
describe("start — session name resolution", () => {
|
|
38
|
+
let tmpDir;
|
|
39
|
+
let origCwd;
|
|
20
40
|
|
|
21
41
|
beforeEach(() => {
|
|
22
42
|
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "kntic-start-test-"));
|
|
@@ -31,34 +51,31 @@ describe("start — getScreenName resolution", () => {
|
|
|
31
51
|
|
|
32
52
|
it("uses KNTIC_PRJ_PREFIX from .kntic.env when present", () => {
|
|
33
53
|
fs.writeFileSync(path.join(tmpDir, ".kntic.env"), "UID=1000\nKNTIC_PRJ_PREFIX=myproject\nGID=1000\n");
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
assert.equal(getScreenName(), "myproject");
|
|
54
|
+
const start = require("./start");
|
|
55
|
+
assert.equal(start.getSessionName(), "myproject");
|
|
37
56
|
});
|
|
38
57
|
|
|
39
58
|
it("falls back to current directory name when KNTIC_PRJ_PREFIX is missing", () => {
|
|
40
59
|
fs.writeFileSync(path.join(tmpDir, ".kntic.env"), "UID=1000\nGID=1000\n");
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
assert.equal(getScreenName(), path.basename(tmpDir));
|
|
60
|
+
const start = require("./start");
|
|
61
|
+
assert.equal(start.getSessionName(), path.basename(tmpDir));
|
|
44
62
|
});
|
|
45
63
|
|
|
46
64
|
it("falls back to current directory name when .kntic.env does not exist", () => {
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
assert.equal(getScreenName(), path.basename(tmpDir));
|
|
65
|
+
const start = require("./start");
|
|
66
|
+
assert.equal(start.getSessionName(), path.basename(tmpDir));
|
|
50
67
|
});
|
|
51
68
|
|
|
52
69
|
it("ignores empty KNTIC_PRJ_PREFIX value", () => {
|
|
53
70
|
fs.writeFileSync(path.join(tmpDir, ".kntic.env"), "KNTIC_PRJ_PREFIX=\nUID=1000\n");
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
assert.equal(getScreenName(), path.basename(tmpDir));
|
|
71
|
+
const start = require("./start");
|
|
72
|
+
assert.equal(start.getSessionName(), path.basename(tmpDir));
|
|
57
73
|
});
|
|
58
74
|
});
|
|
59
75
|
|
|
60
76
|
describe("start — compose provider from .kntic.env", () => {
|
|
61
|
-
let tmpDir
|
|
77
|
+
let tmpDir;
|
|
78
|
+
let origCwd;
|
|
62
79
|
|
|
63
80
|
beforeEach(() => {
|
|
64
81
|
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "kntic-start-compose-"));
|
|
@@ -89,11 +106,13 @@ describe("start — compose provider from .kntic.env", () => {
|
|
|
89
106
|
});
|
|
90
107
|
});
|
|
91
108
|
|
|
92
|
-
describe("start —
|
|
109
|
+
describe("start — session detection helpers", () => {
|
|
93
110
|
let origSTY;
|
|
111
|
+
let origTMUX;
|
|
94
112
|
|
|
95
113
|
beforeEach(() => {
|
|
96
114
|
origSTY = process.env.STY;
|
|
115
|
+
origTMUX = process.env.TMUX;
|
|
97
116
|
});
|
|
98
117
|
|
|
99
118
|
afterEach(() => {
|
|
@@ -102,19 +121,142 @@ describe("start — isInsideScreen detection", () => {
|
|
|
102
121
|
} else {
|
|
103
122
|
process.env.STY = origSTY;
|
|
104
123
|
}
|
|
124
|
+
|
|
125
|
+
if (origTMUX === undefined) {
|
|
126
|
+
delete process.env.TMUX;
|
|
127
|
+
} else {
|
|
128
|
+
process.env.TMUX = origTMUX;
|
|
129
|
+
}
|
|
105
130
|
});
|
|
106
131
|
|
|
107
132
|
it("returns true when STY environment variable is set", () => {
|
|
108
133
|
process.env.STY = "12345.myscreen";
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
assert.equal(isInsideScreen(), true);
|
|
134
|
+
const start = require("./start");
|
|
135
|
+
assert.equal(start.isInsideScreen(), true);
|
|
112
136
|
});
|
|
113
137
|
|
|
114
138
|
it("returns false when STY environment variable is not set", () => {
|
|
115
139
|
delete process.env.STY;
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
140
|
+
const start = require("./start");
|
|
141
|
+
assert.equal(start.isInsideScreen(), false);
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it("returns true when TMUX environment variable is set", () => {
|
|
145
|
+
process.env.TMUX = "/tmp/tmux-1000/default,123,0";
|
|
146
|
+
const start = require("./start");
|
|
147
|
+
assert.equal(start.isInsideTmux(), true);
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it("returns false when TMUX environment variable is not set", () => {
|
|
151
|
+
delete process.env.TMUX;
|
|
152
|
+
const start = require("./start");
|
|
153
|
+
assert.equal(start.isInsideTmux(), false);
|
|
154
|
+
});
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
describe("start — tmux wrapper behavior", () => {
|
|
158
|
+
let tmpDir;
|
|
159
|
+
let origCwd;
|
|
160
|
+
let origTMUX;
|
|
161
|
+
|
|
162
|
+
beforeEach(() => {
|
|
163
|
+
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "kntic-start-tmux-"));
|
|
164
|
+
origCwd = process.cwd();
|
|
165
|
+
origTMUX = process.env.TMUX;
|
|
166
|
+
process.chdir(tmpDir);
|
|
167
|
+
delete process.env.TMUX;
|
|
168
|
+
fs.writeFileSync(path.join(tmpDir, ".kntic.env"), "KNTIC_COMPOSE_PROVIDER=docker compose\nKNTIC_PRJ_PREFIX=demo\n");
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
afterEach(() => {
|
|
172
|
+
process.chdir(origCwd);
|
|
173
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
174
|
+
|
|
175
|
+
if (origTMUX === undefined) {
|
|
176
|
+
delete process.env.TMUX;
|
|
177
|
+
} else {
|
|
178
|
+
process.env.TMUX = origTMUX;
|
|
179
|
+
}
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
it("starts inside tmux when tmux is available and not already inside tmux", () => {
|
|
183
|
+
const calls = [];
|
|
184
|
+
|
|
185
|
+
const logs = captureConsoleLogs(() => {
|
|
186
|
+
withMockedExecSync((command, options) => {
|
|
187
|
+
calls.push({ command, options });
|
|
188
|
+
return Buffer.from("");
|
|
189
|
+
}, (start) => {
|
|
190
|
+
start({ tmux: true });
|
|
191
|
+
});
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
assert.equal(calls.length, 2);
|
|
195
|
+
assert.equal(calls[0].command, "which tmux");
|
|
196
|
+
assert.match(calls[1].command, /^tmux new-session -s 'demo' 'docker compose -f kntic\.yml --env-file \.kntic\.env up --build'$/);
|
|
197
|
+
assert.equal(calls[1].options.stdio, "inherit");
|
|
198
|
+
assert.deepEqual(logs, ['Starting KNTIC services in tmux session "demo"…']);
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
it("falls back to foreground mode when tmux is unavailable", () => {
|
|
202
|
+
const calls = [];
|
|
203
|
+
|
|
204
|
+
const logs = captureConsoleLogs(() => {
|
|
205
|
+
withMockedExecSync((command, options) => {
|
|
206
|
+
calls.push({ command, options });
|
|
207
|
+
if (command === "which tmux") {
|
|
208
|
+
throw new Error("tmux not found");
|
|
209
|
+
}
|
|
210
|
+
return Buffer.from("");
|
|
211
|
+
}, (start) => {
|
|
212
|
+
start({ tmux: true });
|
|
213
|
+
});
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
assert.equal(calls.length, 3);
|
|
217
|
+
assert.equal(calls[0].command, "which tmux");
|
|
218
|
+
assert.equal(calls[1].command, "which tmux");
|
|
219
|
+
assert.equal(calls[2].command, "docker compose -f kntic.yml --env-file .kntic.env up --build");
|
|
220
|
+
assert.deepEqual(logs, [
|
|
221
|
+
"tmux is not available, starting without tmux wrapper.",
|
|
222
|
+
"Starting KNTIC services…",
|
|
223
|
+
]);
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
it("bypasses tmux wrapper when already inside tmux", () => {
|
|
227
|
+
process.env.TMUX = "/tmp/tmux-1000/default,456,0";
|
|
228
|
+
const calls = [];
|
|
229
|
+
|
|
230
|
+
const logs = captureConsoleLogs(() => {
|
|
231
|
+
withMockedExecSync((command, options) => {
|
|
232
|
+
calls.push({ command, options });
|
|
233
|
+
return Buffer.from("");
|
|
234
|
+
}, (start) => {
|
|
235
|
+
start({ tmux: true });
|
|
236
|
+
});
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
assert.equal(calls.length, 2);
|
|
240
|
+
assert.equal(calls[0].command, "which tmux");
|
|
241
|
+
assert.equal(calls[1].command, "docker compose -f kntic.yml --env-file .kntic.env up --build");
|
|
242
|
+
assert.deepEqual(logs, [
|
|
243
|
+
"Already inside a tmux session, skipping tmux wrapper.",
|
|
244
|
+
"Starting KNTIC services…",
|
|
245
|
+
]);
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
it("rejects using --screen and --tmux together", () => {
|
|
249
|
+
const calls = [];
|
|
250
|
+
|
|
251
|
+
assert.throws(() => {
|
|
252
|
+
withMockedExecSync((command) => {
|
|
253
|
+
calls.push(command);
|
|
254
|
+
return Buffer.from("");
|
|
255
|
+
}, (start) => {
|
|
256
|
+
start({ screen: true, tmux: true });
|
|
257
|
+
});
|
|
258
|
+
}, /--screen and --tmux cannot be used together/);
|
|
259
|
+
|
|
260
|
+
assert.deepEqual(calls, []);
|
|
119
261
|
});
|
|
120
262
|
});
|
package/src/commands/update.js
CHANGED
|
@@ -405,60 +405,74 @@ function extractCompose(tarball, destDir) {
|
|
|
405
405
|
console.log("Updated kntic.yml from bootstrap template.");
|
|
406
406
|
}
|
|
407
407
|
|
|
408
|
-
async function update(options = {}) {
|
|
408
|
+
async function update(options = {}, deps = {}) {
|
|
409
409
|
const libOnly = options.libOnly || false;
|
|
410
|
+
const composeFlagUsed = options.compose || false;
|
|
411
|
+
const fetchTextImpl = deps.fetchText || fetchText;
|
|
412
|
+
const downloadImpl = deps.download || download;
|
|
413
|
+
const extractLibOnlyImpl = deps.extractLibOnly || extractLibOnly;
|
|
414
|
+
const extractUpdateImpl = deps.extractUpdate || extractUpdate;
|
|
415
|
+
const mergeEnvFileImpl = deps.mergeEnvFile || mergeEnvFile;
|
|
416
|
+
const extractComposeImpl = deps.extractCompose || extractCompose;
|
|
417
|
+
const updateEnvVersionImpl = deps.updateEnvVersion || updateEnvVersion;
|
|
418
|
+
|
|
419
|
+
if (composeFlagUsed) {
|
|
420
|
+
console.warn("Warning: --compose is deprecated because kntic.yml is now included in the default update path.");
|
|
421
|
+
if (libOnly) {
|
|
422
|
+
console.log("Info: --lib-only skips kntic.yml updates.");
|
|
423
|
+
}
|
|
424
|
+
}
|
|
410
425
|
|
|
411
426
|
// Resolve current version from the artifact metadata file
|
|
412
|
-
const artifactFilename = await
|
|
427
|
+
const artifactFilename = await fetchTextImpl(BOOTSTRAP_ARTIFACT_URL);
|
|
413
428
|
const version = extractVersion(artifactFilename);
|
|
414
429
|
|
|
415
430
|
const tmpFile = path.join(os.tmpdir(), `kntic-bootstrap-${Date.now()}.tar.gz`);
|
|
416
431
|
|
|
417
432
|
console.log(`Downloading KNTIC bootstrap archive… (v${version})`);
|
|
418
|
-
await
|
|
433
|
+
await downloadImpl(BOOTSTRAP_URL, tmpFile);
|
|
419
434
|
|
|
420
435
|
if (libOnly) {
|
|
421
436
|
console.log("Updating .kntic/lib …");
|
|
422
|
-
|
|
437
|
+
extractLibOnlyImpl(tmpFile, ".");
|
|
423
438
|
} else {
|
|
424
439
|
console.log("Updating .kntic/lib, .kntic/adrs, .kntic/hooks/gia/internal, .kntic/hooks/gia/specific (if new), and .kntic/gia/weights.json …");
|
|
425
|
-
|
|
440
|
+
extractUpdateImpl(tmpFile, ".");
|
|
426
441
|
}
|
|
427
442
|
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
const tmpExtractDir = fs.mkdtempSync(path.join(os.tmpdir(), "kntic-env-extract-"));
|
|
443
|
+
if (!libOnly) {
|
|
444
|
+
// Merge new env variables from the archive's .kntic.env template
|
|
445
|
+
const timestamp = Date.now();
|
|
446
|
+
const tmpEnvTemplate = path.join(os.tmpdir(), `kntic-env-template-${timestamp}`);
|
|
433
447
|
try {
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
448
|
+
const tmpExtractDir = fs.mkdtempSync(path.join(os.tmpdir(), "kntic-env-extract-"));
|
|
449
|
+
try {
|
|
450
|
+
execSync(`tar xzf "${tmpFile}" -C "${tmpExtractDir}" "./.kntic.env"`, { stdio: "pipe" });
|
|
451
|
+
const extractedEnv = path.join(tmpExtractDir, ".kntic.env");
|
|
452
|
+
if (fs.existsSync(extractedEnv)) {
|
|
453
|
+
fs.copyFileSync(extractedEnv, tmpEnvTemplate);
|
|
454
|
+
mergeEnvFileImpl(tmpEnvTemplate, ".kntic.env");
|
|
455
|
+
} else {
|
|
456
|
+
console.log("No .kntic.env template in archive, skipping env merge.");
|
|
457
|
+
}
|
|
458
|
+
} catch (_) {
|
|
440
459
|
console.log("No .kntic.env template in archive, skipping env merge.");
|
|
460
|
+
} finally {
|
|
461
|
+
fs.rmSync(tmpExtractDir, { recursive: true, force: true });
|
|
441
462
|
}
|
|
442
|
-
} catch (_) {
|
|
443
|
-
console.log("No .kntic.env template in archive, skipping env merge.");
|
|
444
463
|
} finally {
|
|
445
|
-
fs.
|
|
464
|
+
try { fs.unlinkSync(tmpEnvTemplate); } catch (_) {}
|
|
446
465
|
}
|
|
447
|
-
} finally {
|
|
448
|
-
try { fs.unlinkSync(tmpEnvTemplate); } catch (_) {}
|
|
449
|
-
}
|
|
450
466
|
|
|
451
|
-
// Replace kntic.yml from archive if --compose flag is set
|
|
452
|
-
if (options.compose) {
|
|
453
467
|
try {
|
|
454
|
-
|
|
468
|
+
extractComposeImpl(tmpFile, ".");
|
|
455
469
|
} catch (_) {
|
|
456
470
|
console.log("No kntic.yml in archive, skipping compose update.");
|
|
457
471
|
}
|
|
458
472
|
}
|
|
459
473
|
|
|
460
474
|
// Update KNTIC_VERSION in .kntic.env
|
|
461
|
-
|
|
475
|
+
updateEnvVersionImpl(version);
|
|
462
476
|
|
|
463
477
|
// Clean up
|
|
464
478
|
fs.unlinkSync(tmpFile);
|
|
@@ -467,7 +481,7 @@ async function update(options = {}) {
|
|
|
467
481
|
if (libOnly) {
|
|
468
482
|
console.log("Done. .kntic/lib updated successfully.");
|
|
469
483
|
} else {
|
|
470
|
-
console.log("Done. .kntic/lib, .kntic/adrs, .kntic/hooks/gia/internal, .kntic/hooks/gia/specific (if new),
|
|
484
|
+
console.log("Done. .kntic/lib, .kntic/adrs, .kntic/hooks/gia/internal, .kntic/hooks/gia/specific (if new), .kntic/gia/weights.json, and kntic.yml updated successfully.");
|
|
471
485
|
}
|
|
472
486
|
}
|
|
473
487
|
|
|
@@ -7,7 +7,9 @@ const path = require("path");
|
|
|
7
7
|
const os = require("os");
|
|
8
8
|
const { execSync } = require("child_process");
|
|
9
9
|
|
|
10
|
-
const
|
|
10
|
+
const update = require("./update");
|
|
11
|
+
const usage = require("./usage");
|
|
12
|
+
const { extractLibOnly, extractUpdate, extractCompose, extractVersion, clearDirectory, updateEnvVersion, mergeEnvFile } = update;
|
|
11
13
|
|
|
12
14
|
/**
|
|
13
15
|
* Helper — create a tar.gz archive in `tmpDir` containing the given files.
|
|
@@ -727,6 +729,148 @@ describe("mergeEnvFile", () => {
|
|
|
727
729
|
});
|
|
728
730
|
});
|
|
729
731
|
|
|
732
|
+
describe("update", () => {
|
|
733
|
+
let tmpDir;
|
|
734
|
+
let cwd;
|
|
735
|
+
let originalLog;
|
|
736
|
+
let originalWarn;
|
|
737
|
+
let logs;
|
|
738
|
+
let warnings;
|
|
739
|
+
|
|
740
|
+
beforeEach(() => {
|
|
741
|
+
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "kntic-test-update-flow-"));
|
|
742
|
+
cwd = process.cwd();
|
|
743
|
+
process.chdir(tmpDir);
|
|
744
|
+
logs = [];
|
|
745
|
+
warnings = [];
|
|
746
|
+
originalLog = console.log;
|
|
747
|
+
originalWarn = console.warn;
|
|
748
|
+
console.log = (message) => logs.push(message);
|
|
749
|
+
console.warn = (message) => warnings.push(message);
|
|
750
|
+
});
|
|
751
|
+
|
|
752
|
+
afterEach(() => {
|
|
753
|
+
console.log = originalLog;
|
|
754
|
+
console.warn = originalWarn;
|
|
755
|
+
process.chdir(cwd);
|
|
756
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
757
|
+
});
|
|
758
|
+
|
|
759
|
+
async function runUpdate(options = {}, tarballFiles = {}) {
|
|
760
|
+
const tarball = createTarball(tmpDir, tarballFiles);
|
|
761
|
+
await update(options, {
|
|
762
|
+
fetchText: async () => "kntic-bootstrap-v1.2.3.tar.gz",
|
|
763
|
+
download: async (_url, dest) => fs.copyFileSync(tarball, dest),
|
|
764
|
+
});
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
it("replaces kntic.yml by default and creates a backup", async () => {
|
|
768
|
+
fs.writeFileSync("kntic.yml", "services:\n app:\n image: old\n");
|
|
769
|
+
fs.writeFileSync(".kntic.env", "UID=1000\n");
|
|
770
|
+
|
|
771
|
+
await runUpdate({}, {
|
|
772
|
+
".kntic/lib/orchestrator.py": "# new lib\n",
|
|
773
|
+
".kntic/adrs/ADR-001.md": "# adr\n",
|
|
774
|
+
".kntic.env": "UID=1000\nNEW_VAR=yes\n",
|
|
775
|
+
"kntic.yml": "services:\n app:\n image: new\n",
|
|
776
|
+
});
|
|
777
|
+
|
|
778
|
+
assert.equal(fs.readFileSync("kntic.yml", "utf8"), "services:\n app:\n image: new\n");
|
|
779
|
+
assert.equal(fs.readFileSync("kntic.yml.bak", "utf8"), "services:\n app:\n image: old\n");
|
|
780
|
+
assert.ok(logs.includes("Backing up kntic.yml → kntic.yml.bak"));
|
|
781
|
+
assert.ok(logs.includes("Updated kntic.yml from bootstrap template."));
|
|
782
|
+
});
|
|
783
|
+
|
|
784
|
+
it("prints one deprecation warning for --compose without doing duplicate compose work", async () => {
|
|
785
|
+
fs.writeFileSync("kntic.yml", "services:\n app:\n image: old\n");
|
|
786
|
+
fs.writeFileSync(".kntic.env", "UID=1000\n");
|
|
787
|
+
|
|
788
|
+
await runUpdate({ compose: true }, {
|
|
789
|
+
".kntic/lib/orchestrator.py": "# new lib\n",
|
|
790
|
+
".kntic/adrs/ADR-001.md": "# adr\n",
|
|
791
|
+
".kntic.env": "UID=1000\n",
|
|
792
|
+
"kntic.yml": "services:\n app:\n image: new\n",
|
|
793
|
+
});
|
|
794
|
+
|
|
795
|
+
assert.deepEqual(warnings, [
|
|
796
|
+
"Warning: --compose is deprecated because kntic.yml is now included in the default update path.",
|
|
797
|
+
]);
|
|
798
|
+
assert.equal(
|
|
799
|
+
logs.filter((message) => message === "Backing up kntic.yml → kntic.yml.bak").length,
|
|
800
|
+
1,
|
|
801
|
+
"compose backup must run only once"
|
|
802
|
+
);
|
|
803
|
+
assert.equal(
|
|
804
|
+
logs.filter((message) => message === "Updated kntic.yml from bootstrap template.").length,
|
|
805
|
+
1,
|
|
806
|
+
"compose extraction must run only once"
|
|
807
|
+
);
|
|
808
|
+
});
|
|
809
|
+
|
|
810
|
+
it("keeps --lib-only strict and skips kntic.yml replacement", async () => {
|
|
811
|
+
fs.writeFileSync("kntic.yml", "services:\n app:\n image: old\n");
|
|
812
|
+
fs.writeFileSync(".kntic.env", "UID=1000\n");
|
|
813
|
+
|
|
814
|
+
await runUpdate({ libOnly: true }, {
|
|
815
|
+
".kntic/lib/orchestrator.py": "# new lib\n",
|
|
816
|
+
".kntic/adrs/ADR-001.md": "# adr\n",
|
|
817
|
+
".kntic.env": "UID=1000\nNEW_VAR=yes\n",
|
|
818
|
+
"kntic.yml": "services:\n app:\n image: new\n",
|
|
819
|
+
});
|
|
820
|
+
|
|
821
|
+
assert.equal(fs.readFileSync("kntic.yml", "utf8"), "services:\n app:\n image: old\n");
|
|
822
|
+
assert.ok(!fs.existsSync("kntic.yml.bak"), "backup must not be created in lib-only mode");
|
|
823
|
+
assert.equal(fs.readFileSync(".kntic.env", "utf8"), "UID=1000\nKNTIC_VERSION=1.2.3\n");
|
|
824
|
+
assert.equal(
|
|
825
|
+
logs.filter((message) => message === "Updated kntic.yml from bootstrap template.").length,
|
|
826
|
+
0,
|
|
827
|
+
"compose extraction must be skipped in lib-only mode"
|
|
828
|
+
);
|
|
829
|
+
});
|
|
830
|
+
|
|
831
|
+
it("does not replace kntic.yml for --lib-only --compose, but warns clearly", async () => {
|
|
832
|
+
fs.writeFileSync("kntic.yml", "services:\n app:\n image: old\n");
|
|
833
|
+
fs.writeFileSync(".kntic.env", "UID=1000\n");
|
|
834
|
+
|
|
835
|
+
await runUpdate({ libOnly: true, compose: true }, {
|
|
836
|
+
".kntic/lib/orchestrator.py": "# new lib\n",
|
|
837
|
+
".kntic/adrs/ADR-001.md": "# adr\n",
|
|
838
|
+
".kntic.env": "UID=1000\nNEW_VAR=yes\n",
|
|
839
|
+
"kntic.yml": "services:\n app:\n image: new\n",
|
|
840
|
+
});
|
|
841
|
+
|
|
842
|
+
assert.equal(fs.readFileSync("kntic.yml", "utf8"), "services:\n app:\n image: old\n");
|
|
843
|
+
assert.ok(!fs.existsSync("kntic.yml.bak"), "backup must not be created in lib-only mode");
|
|
844
|
+
assert.deepEqual(warnings, [
|
|
845
|
+
"Warning: --compose is deprecated because kntic.yml is now included in the default update path.",
|
|
846
|
+
]);
|
|
847
|
+
assert.ok(logs.includes("Info: --lib-only skips kntic.yml updates."));
|
|
848
|
+
assert.equal(
|
|
849
|
+
logs.filter((message) => message === "Updated kntic.yml from bootstrap template.").length,
|
|
850
|
+
0,
|
|
851
|
+
"compose extraction must still be skipped when --compose is combined with --lib-only"
|
|
852
|
+
);
|
|
853
|
+
});
|
|
854
|
+
});
|
|
855
|
+
|
|
856
|
+
describe("usage", () => {
|
|
857
|
+
it("documents kntic.yml as part of the default update path and marks --compose deprecated", () => {
|
|
858
|
+
const originalLog = console.log;
|
|
859
|
+
const lines = [];
|
|
860
|
+
console.log = (message) => lines.push(message);
|
|
861
|
+
|
|
862
|
+
try {
|
|
863
|
+
usage();
|
|
864
|
+
} finally {
|
|
865
|
+
console.log = originalLog;
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
assert.ok(lines.some((line) => line.includes("and kntic.yml")), "usage must mention default kntic.yml updates");
|
|
869
|
+
assert.ok(lines.some((line) => line.includes("--compose") && line.includes("Deprecated alias")), "usage must mark --compose deprecated");
|
|
870
|
+
assert.ok(lines.some((line) => line.includes("--lib-only") && line.includes("kntic.yml")), "usage must explain that --lib-only skips kntic.yml");
|
|
871
|
+
});
|
|
872
|
+
});
|
|
873
|
+
|
|
730
874
|
describe("extractCompose", () => {
|
|
731
875
|
let tmpDir;
|
|
732
876
|
let destDir;
|
package/src/commands/usage.js
CHANGED
|
@@ -7,13 +7,14 @@ function usage() {
|
|
|
7
7
|
console.log(" usage List all available sub-commands");
|
|
8
8
|
console.log(" init Download and extract the KNTIC bootstrap template into the current directory");
|
|
9
9
|
console.log(" --interactive Default mode — walk through .kntic.env values interactively (-i)");
|
|
10
|
-
console.log(" --quick Non-interactive mode, auto-detects git remote
|
|
10
|
+
console.log(" --quick Non-interactive mode, auto-detects git remote info and compose provider (-q)");
|
|
11
11
|
console.log(" start Build and start KNTIC services via docker compose (uses kntic.yml + .kntic.env)");
|
|
12
|
-
console.log(" --screen Run inside a GNU screen session");
|
|
12
|
+
console.log(" --screen Run inside a GNU screen session wrapper");
|
|
13
|
+
console.log(" --tmux Run inside a tmux session wrapper (alternative to --screen)");
|
|
13
14
|
console.log(" stop Stop KNTIC services via docker compose");
|
|
14
|
-
console.log(" update Download the latest KNTIC bootstrap and update .kntic/lib, .kntic/adrs, .kntic/hooks/gia/internal, .kntic/hooks/gia/specific (if new),
|
|
15
|
-
console.log(" --lib-only Update only .kntic/lib (
|
|
16
|
-
console.log(" --compose
|
|
15
|
+
console.log(" update Download the latest KNTIC bootstrap and update .kntic/lib, .kntic/adrs, .kntic/hooks/gia/internal, .kntic/hooks/gia/specific (if new), .kntic/gia/weights.json, and kntic.yml");
|
|
16
|
+
console.log(" --lib-only Update only .kntic/lib (skips adrs, hooks, env merge, weights, and kntic.yml)");
|
|
17
|
+
console.log(" --compose Deprecated alias; kntic.yml is already included in the default update path");
|
|
17
18
|
console.log("");
|
|
18
19
|
}
|
|
19
20
|
|