@polderlabs/openkan 0.4.1 → 0.4.3
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/dist/bin/openkan.js +177 -1
- package/package.json +1 -1
package/dist/bin/openkan.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
import { existsSync, readFileSync, writeFileSync, rmSync, appendFileSync, statSync } from "node:fs";
|
|
4
4
|
import { join, dirname, resolve, basename } from "node:path";
|
|
5
5
|
import { fileURLToPath } from "node:url";
|
|
6
|
-
import { spawn } from "node:child_process";
|
|
6
|
+
import { spawn, spawnSync } from "node:child_process";
|
|
7
7
|
import { startOrAttach } from "../kanban/server.js";
|
|
8
8
|
import { addProject, setActiveProject } from "../kanban/projects.js";
|
|
9
9
|
import { initBoard, getBoard, setProjectRoot } from "../kanban/board.js";
|
|
@@ -44,6 +44,33 @@ const AGENT_CAPABILITIES = Object.freeze({
|
|
|
44
44
|
function configPath() {
|
|
45
45
|
return join(process.cwd(), ".ok", "openkan.json");
|
|
46
46
|
}
|
|
47
|
+
// Resolve the running package's package.json so version/installed-from stays
|
|
48
|
+
// accurate even when bin/openkan.mjs is the entrypoint and the .ts/.js lives
|
|
49
|
+
// one or two levels below the package root (src vs dist).
|
|
50
|
+
function installedPackageJson() {
|
|
51
|
+
const here = __dirname;
|
|
52
|
+
for (const dir of [here, join(here, ".."), join(here, "..", "..")]) {
|
|
53
|
+
const candidate = join(dir, "package.json");
|
|
54
|
+
if (existsSync(candidate)) {
|
|
55
|
+
try {
|
|
56
|
+
const parsed = JSON.parse(readFileSync(candidate, "utf8"));
|
|
57
|
+
if (typeof parsed?.name === "string" && typeof parsed?.version === "string") {
|
|
58
|
+
return { name: parsed.name, version: parsed.version };
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
catch { /* fall through to next candidate */ }
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
function printInstalledVersion() {
|
|
67
|
+
const pkg = installedPackageJson();
|
|
68
|
+
if (!pkg) {
|
|
69
|
+
console.log("openkan: version unavailable (no package.json found above the entrypoint)");
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
console.log(`${pkg.name} ${pkg.version}`);
|
|
73
|
+
}
|
|
47
74
|
function loadConfig() {
|
|
48
75
|
const p = configPath();
|
|
49
76
|
if (!existsSync(p))
|
|
@@ -628,6 +655,141 @@ async function cmdReset(ctx, argv) {
|
|
|
628
655
|
removeDir(dir);
|
|
629
656
|
console.log("Reset complete.");
|
|
630
657
|
}
|
|
658
|
+
// ─── Subcommand: update ────────────────────────────────────────────────────────
|
|
659
|
+
async function cmdUpdate(positionals, flags) {
|
|
660
|
+
// Reject unknown flags instead of silently forwarding them to npm — keeps
|
|
661
|
+
// the surface small and predictable.
|
|
662
|
+
const KNOWN_FLAGS = new Set(["check", "yes", "version", "help", "h"]);
|
|
663
|
+
if (flags.help === true || flags.h === true) {
|
|
664
|
+
console.log("Usage: openkan update [--check] [--yes] [--version <semver>]");
|
|
665
|
+
console.log("");
|
|
666
|
+
console.log(" --check Report installed vs latest, exit non-zero if outdated, do not install.");
|
|
667
|
+
console.log(" --yes Skip the interactive confirmation prompt.");
|
|
668
|
+
console.log(" --version <v> Pin the upgrade to a specific semver instead of `latest`.");
|
|
669
|
+
console.log(" -h, --help Show this help.");
|
|
670
|
+
return;
|
|
671
|
+
}
|
|
672
|
+
for (const flag of Object.keys(flags)) {
|
|
673
|
+
if (!KNOWN_FLAGS.has(flag)) {
|
|
674
|
+
console.error(`openkan update: unknown flag --${flag} (known: ${[...KNOWN_FLAGS].map(f => `--${f}`).join(", ")})`);
|
|
675
|
+
process.exit(2);
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
if (positionals.length > 0) {
|
|
679
|
+
console.error(`openkan update: unexpected positional ${positionals[0]} (this command takes no arguments)`);
|
|
680
|
+
process.exit(2);
|
|
681
|
+
}
|
|
682
|
+
const pkg = installedPackageJson();
|
|
683
|
+
if (!pkg) {
|
|
684
|
+
console.error("openkan update: cannot determine the installed package (no package.json found above the entrypoint)");
|
|
685
|
+
process.exit(1);
|
|
686
|
+
}
|
|
687
|
+
// --version <semver> lets scripted callers pin to a known target (e.g.
|
|
688
|
+
// nightly); skip the registry query in that case.
|
|
689
|
+
const pinned = typeof flags.version === "string" ? flags.version : null;
|
|
690
|
+
let target = pinned;
|
|
691
|
+
if (!target) {
|
|
692
|
+
// Query the registry for the latest version on the `latest` dist-tag.
|
|
693
|
+
// We intentionally do not cache or background this; `openkan update`
|
|
694
|
+
// is explicit, infrequent, and the user wants to see the decision.
|
|
695
|
+
target = await npmLatestVersion(pkg.name);
|
|
696
|
+
if (!target) {
|
|
697
|
+
console.error(`openkan update: failed to query the latest version of ${pkg.name} from npm`);
|
|
698
|
+
process.exit(1);
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
if (target === pkg.version) {
|
|
702
|
+
console.log(`${pkg.name} ${pkg.version} is already up to date.`);
|
|
703
|
+
return;
|
|
704
|
+
}
|
|
705
|
+
console.log(`${pkg.name}: installed ${pkg.version}, latest ${target}`);
|
|
706
|
+
if (flags.check) {
|
|
707
|
+
// Just report; the user can re-run without --check to actually upgrade.
|
|
708
|
+
process.exitCode = 1;
|
|
709
|
+
return;
|
|
710
|
+
}
|
|
711
|
+
// Skip the confirmation prompt when --yes was passed (CI / scripted use).
|
|
712
|
+
if (!flags.yes) {
|
|
713
|
+
const proceed = await confirm(`Install ${pkg.name}@${target} now? [Y/n] `);
|
|
714
|
+
if (!proceed) {
|
|
715
|
+
console.log("Cancelled.");
|
|
716
|
+
return;
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
console.log(`Running: npm install -g ${pkg.name}@${target}`);
|
|
720
|
+
const result = spawnSync("npm", ["install", "-g", `${pkg.name}@${target}`], { stdio: "inherit" });
|
|
721
|
+
process.exit(result.status ?? 1);
|
|
722
|
+
}
|
|
723
|
+
async function npmLatestVersion(name) {
|
|
724
|
+
// `npm view <pkg> dist-tags.latest` prints the bare semver string; with
|
|
725
|
+
// --json it wraps that single value in a JSON array. Handle both shapes
|
|
726
|
+
// because some npm versions (and some package fields) emit array output
|
|
727
|
+
// even for scalar fields.
|
|
728
|
+
const stdout = await new Promise((resolve) => {
|
|
729
|
+
const child = spawn("npm", ["view", name, "dist-tags.latest"], { stdio: ["ignore", "pipe", "pipe"] });
|
|
730
|
+
let out = "";
|
|
731
|
+
child.stdout.on("data", (d) => { out += d.toString(); });
|
|
732
|
+
child.on("error", () => resolve(""));
|
|
733
|
+
child.on("close", (code) => { if (code === 0)
|
|
734
|
+
resolve(out);
|
|
735
|
+
else
|
|
736
|
+
resolve(""); });
|
|
737
|
+
}).then(async (text) => {
|
|
738
|
+
if (text.trim())
|
|
739
|
+
return text;
|
|
740
|
+
// Fall back to --json and parse, in case the bare call failed (older
|
|
741
|
+
// npm prints only via --json for dotted paths).
|
|
742
|
+
return await new Promise((resolve) => {
|
|
743
|
+
const child = spawn("npm", ["view", name, "dist-tags.latest", "--json"], { stdio: ["ignore", "pipe", "pipe"] });
|
|
744
|
+
let out = "";
|
|
745
|
+
child.stdout.on("data", (d) => { out += d.toString(); });
|
|
746
|
+
child.on("error", () => resolve(""));
|
|
747
|
+
child.on("close", () => resolve(out));
|
|
748
|
+
});
|
|
749
|
+
});
|
|
750
|
+
const trimmed = stdout.trim();
|
|
751
|
+
if (!trimmed)
|
|
752
|
+
return null;
|
|
753
|
+
// Accept `"0.4.1"` (bare) or `["0.4.1"]` (json-wrapped).
|
|
754
|
+
if (trimmed.startsWith("[")) {
|
|
755
|
+
try {
|
|
756
|
+
const parsed = JSON.parse(trimmed);
|
|
757
|
+
if (Array.isArray(parsed) && typeof parsed[0] === "string")
|
|
758
|
+
return parsed[0];
|
|
759
|
+
}
|
|
760
|
+
catch {
|
|
761
|
+
return null;
|
|
762
|
+
}
|
|
763
|
+
return null;
|
|
764
|
+
}
|
|
765
|
+
if (trimmed.startsWith('"') && trimmed.endsWith('"')) {
|
|
766
|
+
try {
|
|
767
|
+
return JSON.parse(trimmed);
|
|
768
|
+
}
|
|
769
|
+
catch {
|
|
770
|
+
return null;
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
return trimmed;
|
|
774
|
+
}
|
|
775
|
+
async function confirm(question) {
|
|
776
|
+
if (!process.stdin.isTTY) {
|
|
777
|
+
// No TTY → assume yes in the rare case the user piped `openkan update`
|
|
778
|
+
// without --yes; the alternative is silent failure which surprises more.
|
|
779
|
+
return true;
|
|
780
|
+
}
|
|
781
|
+
process.stdout.write(question);
|
|
782
|
+
return await new Promise((resolve) => {
|
|
783
|
+
const onData = (data) => {
|
|
784
|
+
const answer = data.toString().trim().toLowerCase();
|
|
785
|
+
process.stdin.removeListener("data", onData);
|
|
786
|
+
process.stdin.pause();
|
|
787
|
+
resolve(answer === "" || answer === "y" || answer === "yes");
|
|
788
|
+
};
|
|
789
|
+
process.stdin.resume();
|
|
790
|
+
process.stdin.once("data", onData);
|
|
791
|
+
});
|
|
792
|
+
}
|
|
631
793
|
// ─── URL opener ────────────────────────────────────────────────────────────────
|
|
632
794
|
function openUrl(url) {
|
|
633
795
|
const openCmd = process.platform === "win32" ? "start" : process.platform === "darwin" ? "open" : "xdg-open";
|
|
@@ -647,6 +809,7 @@ function printHelp(cmd) {
|
|
|
647
809
|
stop: "stop Stop the running server",
|
|
648
810
|
status: "status Show server status, port, pid, uptime",
|
|
649
811
|
open: "open Open the kanban UI in browser",
|
|
812
|
+
update: "update [--check] [--yes] [--version <v>] Upgrade to the latest @polderlabs/openkan from npm",
|
|
650
813
|
config: "config list|get <key>|set <key> <value> Manage config",
|
|
651
814
|
logs: "logs [--tail N] [--follow] Print server logs",
|
|
652
815
|
api: "api <path> [--method M] [--data JSON|--data-file FILE] Call any local OpenKan REST feature",
|
|
@@ -669,6 +832,7 @@ function printHelp(cmd) {
|
|
|
669
832
|
console.log("Usage: openkan <command> [args...]\n");
|
|
670
833
|
Object.values(msgs).forEach(m => console.log(` ${m}`));
|
|
671
834
|
console.log("\nFlags: --flag=value or --flag value, can appear before or after positionals.");
|
|
835
|
+
console.log("\nVersion: `openkan -v` or `openkan --version` prints the installed package name and version.");
|
|
672
836
|
}
|
|
673
837
|
}
|
|
674
838
|
export async function main(argv = process.argv.slice(2)) {
|
|
@@ -676,6 +840,13 @@ export async function main(argv = process.argv.slice(2)) {
|
|
|
676
840
|
printHelp(argv[0] === "-h" || argv[0] === "--help" ? argv[1] : undefined);
|
|
677
841
|
return;
|
|
678
842
|
}
|
|
843
|
+
// `openkan -v` / `openkan --version` — print the installed package's name
|
|
844
|
+
// and version from its own package.json so the user always sees the truth,
|
|
845
|
+
// even when the shim is rebuilt separately from the TS source.
|
|
846
|
+
if (argv[0] === "-v" || argv[0] === "--version") {
|
|
847
|
+
printInstalledVersion();
|
|
848
|
+
return;
|
|
849
|
+
}
|
|
679
850
|
const { cmd, positionals, flags } = parseArgs(argv);
|
|
680
851
|
if (["task", "plan", "prd", "goal", "progress", "doctor", "index", "migrate-from-openkan"].includes(cmd)) {
|
|
681
852
|
// Help and bare invocations: print the command's help line instead of
|
|
@@ -726,6 +897,11 @@ export async function main(argv = process.argv.slice(2)) {
|
|
|
726
897
|
return;
|
|
727
898
|
}
|
|
728
899
|
// Resolve nested invocations without creating a second workspace.
|
|
900
|
+
// `openkan update` runs npm itself; it must NOT chdir into the nearest
|
|
901
|
+
// .ok/ workspace, must NOT initialise the board, and must NOT need a
|
|
902
|
+
// running server. Handle it before the project-resolution / initBoard paths.
|
|
903
|
+
if (cmd === 'update')
|
|
904
|
+
return cmdUpdate(positionals, flags);
|
|
729
905
|
if (cmd !== 'init') {
|
|
730
906
|
let directory = process.cwd();
|
|
731
907
|
while (!existsSync(join(directory, '.ok')) && dirname(directory) !== directory)
|
package/package.json
CHANGED