@odla-ai/cli 0.41.0 → 0.41.1
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/bin/odla-ai.js +17 -7
- package/bin/stale-build.d.ts +16 -0
- package/bin/stale-build.js +77 -0
- package/bin/workspace-build.d.ts +5 -0
- package/bin/workspace-build.js +16 -4
- package/package.json +1 -1
package/bin/odla-ai.js
CHANGED
|
@@ -1,20 +1,22 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import { existsSync, readFileSync, statSync } from "node:fs";
|
|
3
3
|
import { spawnSync } from "node:child_process";
|
|
4
4
|
import { dirname, join } from "node:path";
|
|
5
5
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
6
6
|
import { workspaceBuildOrder } from "./workspace-build.js";
|
|
7
|
+
import { firstStaleWorkspace, staleBuildNotice } from "./stale-build.js";
|
|
7
8
|
|
|
8
9
|
const packageRoot = join(dirname(fileURLToPath(import.meta.url)), "..");
|
|
9
10
|
const builtEntry = join(packageRoot, "dist", "bin.js");
|
|
11
|
+
const workspaceRoot = join(packageRoot, "..", "..");
|
|
12
|
+
const rootManifestPath = join(workspaceRoot, "package.json");
|
|
13
|
+
const rootManifest = existsSync(rootManifestPath)
|
|
14
|
+
? JSON.parse(readFileSync(rootManifestPath, "utf8"))
|
|
15
|
+
: null;
|
|
16
|
+
const inWorkspace = rootManifest?.name === "odla-ai" && rootManifest.private === true;
|
|
10
17
|
|
|
11
18
|
if (!existsSync(builtEntry)) {
|
|
12
|
-
|
|
13
|
-
const rootManifestPath = join(workspaceRoot, "package.json");
|
|
14
|
-
const rootManifest = existsSync(rootManifestPath)
|
|
15
|
-
? JSON.parse(readFileSync(rootManifestPath, "utf8"))
|
|
16
|
-
: null;
|
|
17
|
-
if (rootManifest?.name !== "odla-ai" || rootManifest.private !== true) {
|
|
19
|
+
if (!inWorkspace) {
|
|
18
20
|
throw new Error("odla-ai CLI build is missing; reinstall @odla-ai/cli from npm");
|
|
19
21
|
}
|
|
20
22
|
|
|
@@ -28,6 +30,14 @@ if (!existsSync(builtEntry)) {
|
|
|
28
30
|
);
|
|
29
31
|
if (child.error) throw child.error;
|
|
30
32
|
if (child.status !== 0) process.exit(child.status ?? 1);
|
|
33
|
+
} else if (inWorkspace) {
|
|
34
|
+
// stderr, never stdout: every --json command stays parseable.
|
|
35
|
+
const stale = firstStaleWorkspace(
|
|
36
|
+
workspaceRoot,
|
|
37
|
+
workspaceBuildOrder(workspaceRoot),
|
|
38
|
+
statSync(builtEntry).mtimeMs,
|
|
39
|
+
);
|
|
40
|
+
if (stale) console.error(staleBuildNotice(stale));
|
|
31
41
|
}
|
|
32
42
|
|
|
33
43
|
await import(pathToFileURL(builtEntry).href);
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/** A workspace whose sources are newer than the built entry about to run. */
|
|
2
|
+
export interface StaleWorkspace {
|
|
3
|
+
workspace: string;
|
|
4
|
+
file: string;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
/** The first workspace in `workspaceNames` whose `src` is newer than
|
|
8
|
+
* `builtAtMs`, or null when the build is current or the tree is unreadable. */
|
|
9
|
+
export declare function firstStaleWorkspace(
|
|
10
|
+
root: string,
|
|
11
|
+
workspaceNames: readonly string[],
|
|
12
|
+
builtAtMs: number,
|
|
13
|
+
): StaleWorkspace | null;
|
|
14
|
+
|
|
15
|
+
/** The one-line stderr notice naming the stale workspace and the way out. */
|
|
16
|
+
export declare function staleBuildNotice(stale: StaleWorkspace): string;
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
// A workspace checkout runs the CLI through a symlink into packages/cli, so
|
|
2
|
+
// `npx odla-ai` answers from whatever was last built — which can be a day older
|
|
3
|
+
// than the source sitting next to it, with nothing saying so. That is not a
|
|
4
|
+
// cosmetic problem: an agent reads the stale output, believes it, and reasons
|
|
5
|
+
// from a surface that no longer exists. The launcher already handles "not built
|
|
6
|
+
// at all"; this handles "built, but behind".
|
|
7
|
+
import { existsSync, readdirSync, statSync } from "node:fs";
|
|
8
|
+
import { join } from "node:path";
|
|
9
|
+
import { workspacePackages } from "./workspace-build.js";
|
|
10
|
+
|
|
11
|
+
/** Directories that never hold sources worth comparing. */
|
|
12
|
+
const SKIP = new Set(["node_modules", "dist", "js", ".turbo", ".wrangler"]);
|
|
13
|
+
|
|
14
|
+
/** First file under `dir` modified after `builtAtMs`, or null. Depth-first and
|
|
15
|
+
* early-exiting: the answer is "is anything newer", not "what is newest". */
|
|
16
|
+
function firstNewerFile(dir, builtAtMs) {
|
|
17
|
+
let entries;
|
|
18
|
+
try {
|
|
19
|
+
entries = readdirSync(dir, { withFileTypes: true });
|
|
20
|
+
} catch {
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
for (const entry of entries) {
|
|
24
|
+
if (entry.name.startsWith(".") || SKIP.has(entry.name)) continue;
|
|
25
|
+
const path = join(dir, entry.name);
|
|
26
|
+
if (entry.isDirectory()) {
|
|
27
|
+
const found = firstNewerFile(path, builtAtMs);
|
|
28
|
+
if (found) return found;
|
|
29
|
+
continue;
|
|
30
|
+
}
|
|
31
|
+
try {
|
|
32
|
+
if (statSync(path).mtimeMs > builtAtMs) return path;
|
|
33
|
+
} catch {
|
|
34
|
+
// A file that vanished mid-walk is not evidence of staleness.
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* The first workspace whose sources are newer than the built entry about to be
|
|
42
|
+
* run, or null when the build is current.
|
|
43
|
+
*
|
|
44
|
+
* Checks the CLI's whole internal dependency closure, not just its own `src`:
|
|
45
|
+
* a stale `@odla-ai/db` breaks the CLI just as thoroughly, and reports itself
|
|
46
|
+
* as a missing export rather than as a stale build.
|
|
47
|
+
*
|
|
48
|
+
* Returns null rather than throwing for anything unreadable — a diagnostic
|
|
49
|
+
* must never be the reason a command fails.
|
|
50
|
+
*/
|
|
51
|
+
export function firstStaleWorkspace(root, workspaceNames, builtAtMs) {
|
|
52
|
+
try {
|
|
53
|
+
const packages = workspacePackages(root);
|
|
54
|
+
for (const name of workspaceNames) {
|
|
55
|
+
const entry = packages.get(name);
|
|
56
|
+
if (!entry) continue;
|
|
57
|
+
// A published install ships `dist` and no `src`, so its absence is how we
|
|
58
|
+
// know this is not a source checkout rather than a stale one.
|
|
59
|
+
const source = join(entry.dir, "src");
|
|
60
|
+
if (!existsSync(source)) continue;
|
|
61
|
+
const file = firstNewerFile(source, builtAtMs);
|
|
62
|
+
if (file) return { workspace: name, file };
|
|
63
|
+
}
|
|
64
|
+
} catch {
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** The one line a stale workspace build is worth, and what ends it. */
|
|
71
|
+
export function staleBuildNotice(stale) {
|
|
72
|
+
return (
|
|
73
|
+
`odla-ai: running a stale build — ${stale.workspace} source is newer than ` +
|
|
74
|
+
"the built CLI, so this output can describe code that no longer exists. " +
|
|
75
|
+
"Run `npm run build`."
|
|
76
|
+
);
|
|
77
|
+
}
|
package/bin/workspace-build.d.ts
CHANGED
|
@@ -1,2 +1,7 @@
|
|
|
1
1
|
/** Return buildable internal dependencies in dependency-first order. */
|
|
2
2
|
export declare function workspaceBuildOrder(root: string): string[];
|
|
3
|
+
|
|
4
|
+
/** Every workspace in the checkout, by package name, with its directory. */
|
|
5
|
+
export declare function workspacePackages(
|
|
6
|
+
root: string,
|
|
7
|
+
): Map<string, { dir: string; manifest: Record<string, unknown> }>;
|
package/bin/workspace-build.js
CHANGED
|
@@ -3,8 +3,10 @@ import { join } from "node:path";
|
|
|
3
3
|
|
|
4
4
|
const manifestAt = (path) => JSON.parse(readFileSync(path, "utf8"));
|
|
5
5
|
|
|
6
|
-
/**
|
|
7
|
-
|
|
6
|
+
/** Every workspace in the checkout, by package name, with the directory it
|
|
7
|
+
* lives in. Shared so the launcher's build order and its staleness check agree
|
|
8
|
+
* on what a workspace is and where its sources are. */
|
|
9
|
+
export function workspacePackages(root) {
|
|
8
10
|
const rootManifest = manifestAt(join(root, "package.json"));
|
|
9
11
|
const workspaces = new Map();
|
|
10
12
|
for (const pattern of rootManifest.workspaces ?? []) {
|
|
@@ -13,12 +15,22 @@ export function workspaceBuildOrder(root) {
|
|
|
13
15
|
if (!existsSync(parent)) continue;
|
|
14
16
|
for (const entry of readdirSync(parent, { withFileTypes: true })) {
|
|
15
17
|
if (!entry.isDirectory()) continue;
|
|
16
|
-
const
|
|
18
|
+
const dir = join(parent, entry.name);
|
|
19
|
+
const manifestPath = join(dir, "package.json");
|
|
17
20
|
if (!existsSync(manifestPath)) continue;
|
|
18
21
|
const manifest = manifestAt(manifestPath);
|
|
19
|
-
if (manifest.name) workspaces.set(manifest.name, manifest);
|
|
22
|
+
if (manifest.name) workspaces.set(manifest.name, { dir, manifest });
|
|
20
23
|
}
|
|
21
24
|
}
|
|
25
|
+
return workspaces;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Return buildable internal dependencies in dependency-first order. */
|
|
29
|
+
export function workspaceBuildOrder(root) {
|
|
30
|
+
const packages = workspacePackages(root);
|
|
31
|
+
const workspaces = new Map(
|
|
32
|
+
[...packages].map(([name, entry]) => [name, entry.manifest]),
|
|
33
|
+
);
|
|
22
34
|
|
|
23
35
|
const visited = new Set();
|
|
24
36
|
const order = [];
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@odla-ai/cli",
|
|
3
|
-
"version": "0.41.
|
|
3
|
+
"version": "0.41.1",
|
|
4
4
|
"description": "Agent-operable CLI for odla provisioning, calendar consent and connection lifecycle, System AI administration, Worker secrets, security jobs, and smoke checks.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"homepage": "https://odla.ai/docs/packages/cli",
|