@astrale-os/cli 1.0.0-beta.30 → 1.0.0-beta.31
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/README.md +5 -1
- package/dist/astrale.js +51 -15
- package/package.json +1 -1
- package/src/commands/__tests__/read-commands.test.ts +2 -2
- package/src/commands/query.ts +5 -5
- package/src/commands/update.ts +3 -2
- package/src/graph/.spec/api.d.ts +1 -1
- package/src/graph/.spec/laws/documents.ts +7 -7
- package/src/graph/__tests__/query.test.ts +12 -5
- package/src/graph/query.ts +9 -11
- package/src/lib/__tests__/skills.test.ts +27 -2
- package/src/lib/skills/sync.ts +72 -8
- package/src/program/__tests__/program.test.ts +39 -2
- package/studio/server/cli-consumers.test.ts +1 -1
- package/studio/server/cli.test.ts +2 -2
- package/studio/server/views/target.ts +1 -1
package/README.md
CHANGED
|
@@ -105,7 +105,11 @@ astrale update
|
|
|
105
105
|
from `astrale-os/cli` is installed and healthy, and follows the beta channel by default. It reports
|
|
106
106
|
whether the skill pass was unchanged, installed, updated, or repaired. Use `--check`,
|
|
107
107
|
`--channel <channel>`, or `--version <version>` to control the CLI release target; skills always
|
|
108
|
-
|
|
108
|
+
resolve the repository's current `main` once, verify the installed trees against that commit, and
|
|
109
|
+
stamp the exact revision/tree binding into the Astrale-owned receipt. A current
|
|
110
|
+
`astrale update --check --json` result includes the resolved source revision plus every shipped
|
|
111
|
+
skill's Git tree and entrypoint, so runners can retain the actual loadout rather than only
|
|
112
|
+
`status: current`. `--no-skills` is the explicit opt-out.
|
|
109
113
|
|
|
110
114
|
## Commands
|
|
111
115
|
|
package/dist/astrale.js
CHANGED
|
@@ -2578,7 +2578,7 @@ var package_default;
|
|
|
2578
2578
|
var init_package = __esm(() => {
|
|
2579
2579
|
package_default = {
|
|
2580
2580
|
name: "@astrale-os/cli",
|
|
2581
|
-
version: "1.0.0-beta.
|
|
2581
|
+
version: "1.0.0-beta.31",
|
|
2582
2582
|
description: "Astrale CLI — connect to existing Astrale kernels",
|
|
2583
2583
|
keywords: [
|
|
2584
2584
|
"astrale",
|
|
@@ -61700,7 +61700,7 @@ async function inspectAstraleSkills(snapshot2, home, lockPath) {
|
|
|
61700
61700
|
}
|
|
61701
61701
|
}));
|
|
61702
61702
|
if (managed.length === 0 && expectedPresence.every((present2) => !present2)) {
|
|
61703
|
-
return { state: "absent", managedNames: [], managedFolders: [] };
|
|
61703
|
+
return { state: "absent", sourceCurrent: false, managedNames: [], managedFolders: [] };
|
|
61704
61704
|
}
|
|
61705
61705
|
const folders = managed.flatMap(([name, entry]) => {
|
|
61706
61706
|
const folder = folderName(entry);
|
|
@@ -61742,12 +61742,17 @@ async function inspectAstraleSkills(snapshot2, home, lockPath) {
|
|
|
61742
61742
|
}
|
|
61743
61743
|
}
|
|
61744
61744
|
}
|
|
61745
|
-
const
|
|
61745
|
+
const sourceCurrent = coherent && managed.length === snapshot2.skills.length && folders.every(({ key, folder, entry }) => {
|
|
61746
61746
|
const skill = expected.get(folder);
|
|
61747
61747
|
return key === folder && skill !== undefined && entry.ref === snapshot2.ref && actualHashes.get(folder) === skill.tree;
|
|
61748
61748
|
});
|
|
61749
|
+
const exactCurrent = sourceCurrent && folders.every(({ folder, entry }) => {
|
|
61750
|
+
const skill = expected.get(folder);
|
|
61751
|
+
return skill !== undefined && entry.astraleSourceRevision === snapshot2.revision && entry.astraleSourceTree === skill.tree;
|
|
61752
|
+
});
|
|
61749
61753
|
return {
|
|
61750
61754
|
state: exactCurrent ? "current" : coherent ? "outdated" : "unhealthy",
|
|
61755
|
+
sourceCurrent,
|
|
61751
61756
|
managedNames: managed.map(([name]) => name),
|
|
61752
61757
|
managedFolders: [...uniqueFolders]
|
|
61753
61758
|
};
|
|
@@ -61768,6 +61773,20 @@ function installerArgs(...args) {
|
|
|
61768
61773
|
async function installSnapshot(snapshot2, execute, selectedAgents) {
|
|
61769
61774
|
return await execute("npx", installerArgs("add", `${ASTRALE_CLI_SKILL_SOURCE}#${snapshot2.ref}`, "-g", "-y", "--skill", ...snapshot2.skills.map((skill) => skill.name), ...selectedAgents.length > 0 ? ["--agent", ...selectedAgents] : []));
|
|
61770
61775
|
}
|
|
61776
|
+
async function stampAstraleSource(snapshot2, lockPath) {
|
|
61777
|
+
const { lock } = await readSkillLock(lockPath);
|
|
61778
|
+
if (!lock)
|
|
61779
|
+
throw new Error("skill installer receipt is unavailable after installation");
|
|
61780
|
+
for (const skill of snapshot2.skills) {
|
|
61781
|
+
const entry = lock.skills[skill.name];
|
|
61782
|
+
if (!entry || !sourceOwned(entry) || entry.ref !== snapshot2.ref || entry.skillPath !== skill.path) {
|
|
61783
|
+
throw new Error(`skill installer receipt is incomplete for ${skill.name}`);
|
|
61784
|
+
}
|
|
61785
|
+
entry.astraleSourceRevision = snapshot2.revision;
|
|
61786
|
+
entry.astraleSourceTree = skill.tree;
|
|
61787
|
+
}
|
|
61788
|
+
await writeSkillLock(lockPath, lock);
|
|
61789
|
+
}
|
|
61771
61790
|
async function selectedAgents(home, lockPath) {
|
|
61772
61791
|
const { lock } = await readSkillLock(lockPath);
|
|
61773
61792
|
const agents = new Set((lock?.lastSelectedAgents ?? []).filter((agent) => typeof agent === "string" && SAFE_NAME.test(agent)));
|
|
@@ -61989,8 +62008,20 @@ async function checkAstraleSkills(overrides = {}) {
|
|
|
61989
62008
|
try {
|
|
61990
62009
|
const snapshot2 = await sourceSnapshot(dependencies);
|
|
61991
62010
|
const inspection = await inspectAstraleSkills(snapshot2, dependencies.home, dependencies.lockPath);
|
|
62011
|
+
const status = inspection.state === "current" ? "current" : inspection.state === "unhealthy" ? "repair-needed" : "update-available";
|
|
61992
62012
|
return {
|
|
61993
|
-
status
|
|
62013
|
+
status,
|
|
62014
|
+
...status === "current" ? {
|
|
62015
|
+
source: {
|
|
62016
|
+
repository: ASTRALE_CLI_SKILL_SOURCE,
|
|
62017
|
+
revision: snapshot2.revision,
|
|
62018
|
+
skills: snapshot2.skills.map(({ name, tree, path: path4 }) => ({
|
|
62019
|
+
name,
|
|
62020
|
+
tree,
|
|
62021
|
+
entrypoint: path4
|
|
62022
|
+
}))
|
|
62023
|
+
}
|
|
62024
|
+
} : {}
|
|
61994
62025
|
};
|
|
61995
62026
|
} catch (error52) {
|
|
61996
62027
|
return { status: "unavailable", error: error52 instanceof Error ? error52.message : String(error52) };
|
|
@@ -62040,6 +62071,11 @@ async function syncAstraleSkills(overrides = {}) {
|
|
|
62040
62071
|
throw new Error(installed.stderr || installed.stdout || "skill installer failed");
|
|
62041
62072
|
}
|
|
62042
62073
|
await pruneObsoleteEntries(snapshot2, dependencies.home, dependencies.lockPath, dependencies.run, knownRetired);
|
|
62074
|
+
const installedInspection = await inspectAstraleSkills(snapshot2, dependencies.home, dependencies.lockPath);
|
|
62075
|
+
if (!installedInspection.sourceCurrent) {
|
|
62076
|
+
throw new Error("installed Astrale skills do not match the resolved source");
|
|
62077
|
+
}
|
|
62078
|
+
await stampAstraleSource(snapshot2, dependencies.lockPath);
|
|
62043
62079
|
const verified = await inspectAstraleSkills(snapshot2, dependencies.home, dependencies.lockPath);
|
|
62044
62080
|
if (verified.state !== "current") {
|
|
62045
62081
|
throw new Error("installed Astrale skills did not pass verification");
|
|
@@ -82689,7 +82725,7 @@ Behavior:
|
|
|
82689
82725
|
script installs only — if Astrale was installed by another package manager this
|
|
82690
82726
|
command refuses so that manager stays in charge; downloads are checksum-verified
|
|
82691
82727
|
before the binary is replaced. (2) The Astrale agent skills: installs every
|
|
82692
|
-
top-level skill published from astrale-os/cli main, updates healthy older
|
|
82728
|
+
top-level skill published from one resolved astrale-os/cli main commit, updates healthy older
|
|
82693
82729
|
installs, repairs inconsistent installs, and verifies the result before
|
|
82694
82730
|
reporting success. (3) SDK deps: inside a pnpm domain
|
|
82695
82731
|
project, proposes any @astrale-os/* dependency with a newer release and, on
|
|
@@ -83288,20 +83324,20 @@ var init_query5 = __esm(() => {
|
|
|
83288
83324
|
function prepareQuery(input) {
|
|
83289
83325
|
const limit = positiveInteger2(input.limit, "--limit", DEFAULT_LIMIT);
|
|
83290
83326
|
if (input.ast !== undefined) {
|
|
83291
|
-
if (input.sources.length > 0 || input.
|
|
83292
|
-
throw new TypeError("--ast/--file cannot be combined with sources, --
|
|
83327
|
+
if (input.sources.length > 0 || input.class !== undefined || input.edge !== undefined || input.direction !== undefined) {
|
|
83328
|
+
throw new TypeError("--ast/--file cannot be combined with sources, --class, --edge, or --direction");
|
|
83293
83329
|
}
|
|
83294
83330
|
return withPage(QueryAST.decode(input.ast), limit, input.cursor);
|
|
83295
83331
|
}
|
|
83296
|
-
if (input.sources.length === 0 && input.
|
|
83297
|
-
throw new TypeError("query requires a Path source, --
|
|
83332
|
+
if (input.sources.length === 0 && input.class === undefined) {
|
|
83333
|
+
throw new TypeError("query requires a Path source, --class, or a canonical Query V6 document");
|
|
83298
83334
|
}
|
|
83299
83335
|
if (input.direction !== undefined && input.edge === undefined) {
|
|
83300
83336
|
throw new TypeError("--direction requires --edge");
|
|
83301
83337
|
}
|
|
83302
83338
|
const paths3 = input.sources.map((source2) => Path.parse(source2));
|
|
83303
|
-
const
|
|
83304
|
-
const nodes =
|
|
83339
|
+
const selectedClass = input.class === undefined ? undefined : classReference(input.class, "--class");
|
|
83340
|
+
const nodes = selectedClass === undefined ? paths3 : [...paths3, selectedClass];
|
|
83305
83341
|
const query4 = Query.from({
|
|
83306
83342
|
nodes
|
|
83307
83343
|
});
|
|
@@ -83477,7 +83513,7 @@ async function queryCommand(sources2, opts) {
|
|
|
83477
83513
|
input = {
|
|
83478
83514
|
sources: sources2,
|
|
83479
83515
|
...ast === undefined ? {} : { ast },
|
|
83480
|
-
|
|
83516
|
+
class: opts.class,
|
|
83481
83517
|
edge: opts.edge,
|
|
83482
83518
|
direction: opts.direction,
|
|
83483
83519
|
limit: opts.limit,
|
|
@@ -83547,7 +83583,7 @@ var init_query7 = __esm(() => {
|
|
|
83547
83583
|
description: "Run one canonical Query V6 graph read",
|
|
83548
83584
|
afterHelpText: `
|
|
83549
83585
|
Behavior:
|
|
83550
|
-
Positional Paths and --
|
|
83586
|
+
Positional Paths and --class author a finite Query V6 read. --edge adds
|
|
83551
83587
|
one exact Edge-Class expansion; --direction defaults to outgoing. --ast and
|
|
83552
83588
|
--file accept a complete canonical astrale.graph.query/v6 document, including
|
|
83553
83589
|
Property ordering and Node or Edge reference/value projections. --cursor resumes
|
|
@@ -83559,7 +83595,7 @@ Behavior:
|
|
|
83559
83595
|
|
|
83560
83596
|
Examples:
|
|
83561
83597
|
$ astrale query /:notes.example.dev:class.Note --limit 50
|
|
83562
|
-
$ astrale query --
|
|
83598
|
+
$ astrale query --class /:notes.example.dev:class.Note --limit 50
|
|
83563
83599
|
$ astrale query @note --edge /:notes.example.dev:class.references --direction outgoing --limit 25
|
|
83564
83600
|
$ astrale query --file query.v6.json --cursor "$CURSOR"
|
|
83565
83601
|
`,
|
|
@@ -83568,7 +83604,7 @@ Examples:
|
|
|
83568
83604
|
{ flags: "--ast <json>", description: "Canonical Query V6 JSON document" },
|
|
83569
83605
|
{ flags: "-f, --file <path>", description: "Read a canonical Query V6 document from a file" },
|
|
83570
83606
|
{
|
|
83571
|
-
flags: "--
|
|
83607
|
+
flags: "--class <path>",
|
|
83572
83608
|
description: "Select Nodes satisfying one exact Class"
|
|
83573
83609
|
},
|
|
83574
83610
|
{ flags: "--edge <class>", description: "Expand one exact Edge Class" },
|
package/package.json
CHANGED
|
@@ -195,7 +195,7 @@ describe('query command', () => {
|
|
|
195
195
|
|
|
196
196
|
await queryCommand([], {
|
|
197
197
|
json: true,
|
|
198
|
-
|
|
198
|
+
class: '/:issues.astrale.ai:class.Issue',
|
|
199
199
|
limit: '20',
|
|
200
200
|
})
|
|
201
201
|
|
|
@@ -239,7 +239,7 @@ describe('query command', () => {
|
|
|
239
239
|
|
|
240
240
|
await queryCommand([], {
|
|
241
241
|
json: true,
|
|
242
|
-
|
|
242
|
+
class: '/:issues.astrale.ai:class.Issue',
|
|
243
243
|
limit: '1',
|
|
244
244
|
})
|
|
245
245
|
|
package/src/commands/query.ts
CHANGED
|
@@ -14,7 +14,7 @@ import { isMachine, output } from '../lib/output'
|
|
|
14
14
|
type QueryOpts = KernelCommandOpts & {
|
|
15
15
|
ast?: string
|
|
16
16
|
file?: string
|
|
17
|
-
|
|
17
|
+
class?: string
|
|
18
18
|
edge?: string
|
|
19
19
|
direction?: QueryDirection
|
|
20
20
|
limit?: string
|
|
@@ -28,7 +28,7 @@ export async function queryCommand(sources: string[], opts: QueryOpts): Promise<
|
|
|
28
28
|
input = {
|
|
29
29
|
sources,
|
|
30
30
|
...(ast === undefined ? {} : { ast }),
|
|
31
|
-
|
|
31
|
+
class: opts.class,
|
|
32
32
|
edge: opts.edge,
|
|
33
33
|
direction: opts.direction,
|
|
34
34
|
limit: opts.limit,
|
|
@@ -103,7 +103,7 @@ export default {
|
|
|
103
103
|
description: 'Run one canonical Query V6 graph read',
|
|
104
104
|
afterHelpText: `
|
|
105
105
|
Behavior:
|
|
106
|
-
Positional Paths and --
|
|
106
|
+
Positional Paths and --class author a finite Query V6 read. --edge adds
|
|
107
107
|
one exact Edge-Class expansion; --direction defaults to outgoing. --ast and
|
|
108
108
|
--file accept a complete canonical astrale.graph.query/v6 document, including
|
|
109
109
|
Property ordering and Node or Edge reference/value projections. --cursor resumes
|
|
@@ -115,7 +115,7 @@ Behavior:
|
|
|
115
115
|
|
|
116
116
|
Examples:
|
|
117
117
|
$ astrale query /:notes.example.dev:class.Note --limit 50
|
|
118
|
-
$ astrale query --
|
|
118
|
+
$ astrale query --class /:notes.example.dev:class.Note --limit 50
|
|
119
119
|
$ astrale query @note --edge /:notes.example.dev:class.references --direction outgoing --limit 25
|
|
120
120
|
$ astrale query --file query.v6.json --cursor "$CURSOR"
|
|
121
121
|
`,
|
|
@@ -124,7 +124,7 @@ Examples:
|
|
|
124
124
|
{ flags: '--ast <json>', description: 'Canonical Query V6 JSON document' },
|
|
125
125
|
{ flags: '-f, --file <path>', description: 'Read a canonical Query V6 document from a file' },
|
|
126
126
|
{
|
|
127
|
-
flags: '--
|
|
127
|
+
flags: '--class <path>',
|
|
128
128
|
description: 'Select Nodes satisfying one exact Class',
|
|
129
129
|
},
|
|
130
130
|
{ flags: '--edge <class>', description: 'Expand one exact Edge Class' },
|
package/src/commands/update.ts
CHANGED
|
@@ -112,7 +112,8 @@ async function refreshSdkDeps(check: boolean, assumeYes = false): Promise<boolea
|
|
|
112
112
|
* unified and NON-THROWING: an explicit package-managed result uses npm release
|
|
113
113
|
* identity, while script-install failures remain script failures in `error`
|
|
114
114
|
* instead of being recategorized. The SDK axis is already best-effort. Skills
|
|
115
|
-
* report meaningful health/freshness states
|
|
115
|
+
* report meaningful health/freshness states. A current cohort also exposes its exact
|
|
116
|
+
* source revision, skill trees, and entrypoints without leaking installer-local paths.
|
|
116
117
|
*/
|
|
117
118
|
export type StaleReport = {
|
|
118
119
|
stale: boolean
|
|
@@ -237,7 +238,7 @@ Behavior:
|
|
|
237
238
|
script installs only — if Astrale was installed by another package manager this
|
|
238
239
|
command refuses so that manager stays in charge; downloads are checksum-verified
|
|
239
240
|
before the binary is replaced. (2) The Astrale agent skills: installs every
|
|
240
|
-
top-level skill published from astrale-os/cli main, updates healthy older
|
|
241
|
+
top-level skill published from one resolved astrale-os/cli main commit, updates healthy older
|
|
241
242
|
installs, repairs inconsistent installs, and verifies the result before
|
|
242
243
|
reporting success. (3) SDK deps: inside a pnpm domain
|
|
243
244
|
project, proposes any @astrale-os/* dependency with a newer release and, on
|
package/src/graph/.spec/api.d.ts
CHANGED
|
@@ -11,7 +11,7 @@ export function classKey(input: string, label: string): ClassKey
|
|
|
11
11
|
/** Untrusted CLI fields used to author or admit one exact Query V6 request. */
|
|
12
12
|
export interface QueryCommandInput {
|
|
13
13
|
readonly sources: readonly string[]
|
|
14
|
-
readonly
|
|
14
|
+
readonly class?: string
|
|
15
15
|
readonly ast?: unknown
|
|
16
16
|
readonly edge?: string
|
|
17
17
|
readonly direction?: QueryDirection
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
import { defineLaw } from '@astrale-os/spec/authoring'
|
|
2
2
|
|
|
3
|
-
export const
|
|
4
|
-
id: 'CLI-GRAPH-QUERY-
|
|
3
|
+
export const CLI_GRAPH_QUERY_V6 = defineLaw({
|
|
4
|
+
id: 'CLI-GRAPH-QUERY-V6',
|
|
5
5
|
statement:
|
|
6
|
-
'CLI query input becomes exactly one canonical Query
|
|
6
|
+
'CLI query input becomes exactly one canonical Query V6 document with an explicit finite limit; --class authors one exact Class source, exact Property ordering and Node or Edge projection profiles are admitted through the canonical AST surface, and legacy versions or unsupported selector combinations fail before a Graph call.',
|
|
7
7
|
tests: [
|
|
8
|
-
{ file: '__tests__/query.test.ts', id: 'TEST-CLI-GRAPH-AUTHORS-QUERY-
|
|
9
|
-
{ file: '__tests__/query.test.ts', id: 'TEST-CLI-GRAPH-AUTHORS-
|
|
10
|
-
{ file: '__tests__/query.test.ts', id: 'TEST-CLI-GRAPH-ADMITS-QUERY-
|
|
11
|
-
{ file: '__tests__/query.test.ts', id: 'TEST-CLI-GRAPH-ADMITS-QUERY-
|
|
8
|
+
{ file: '__tests__/query.test.ts', id: 'TEST-CLI-GRAPH-AUTHORS-QUERY-V6' },
|
|
9
|
+
{ file: '__tests__/query.test.ts', id: 'TEST-CLI-GRAPH-AUTHORS-CLASS-QUERY' },
|
|
10
|
+
{ file: '__tests__/query.test.ts', id: 'TEST-CLI-GRAPH-ADMITS-QUERY-V6-ORDERING' },
|
|
11
|
+
{ file: '__tests__/query.test.ts', id: 'TEST-CLI-GRAPH-ADMITS-QUERY-V6-PROJECTIONS' },
|
|
12
12
|
{ file: '__tests__/query.test.ts', id: 'TEST-CLI-GRAPH-REJECTS-LEGACY-QUERY' },
|
|
13
13
|
],
|
|
14
14
|
})
|
|
@@ -37,11 +37,11 @@ describe('prepareQuery', () => {
|
|
|
37
37
|
})
|
|
38
38
|
})
|
|
39
39
|
|
|
40
|
-
/** @evidence TEST-CLI-GRAPH-AUTHORS-
|
|
40
|
+
/** @evidence TEST-CLI-GRAPH-AUTHORS-CLASS-QUERY */
|
|
41
41
|
test('authors an exact Class source without backend query text', () => {
|
|
42
42
|
const prepared = prepareQuery({
|
|
43
43
|
sources: [],
|
|
44
|
-
|
|
44
|
+
class: '/:issues.astrale.ai:class.Issue',
|
|
45
45
|
limit: '201',
|
|
46
46
|
})
|
|
47
47
|
|
|
@@ -67,7 +67,7 @@ describe('prepareQuery', () => {
|
|
|
67
67
|
test('unions positional Paths and one Class source in authored order', () => {
|
|
68
68
|
const prepared = prepareQuery({
|
|
69
69
|
sources: ['@note'],
|
|
70
|
-
|
|
70
|
+
class: '/:issues.astrale.ai:class.Issue',
|
|
71
71
|
})
|
|
72
72
|
|
|
73
73
|
expect(JSON.parse(JSON.stringify(prepared.ast.source))).toEqual({
|
|
@@ -167,7 +167,14 @@ describe('prepareQuery', () => {
|
|
|
167
167
|
prepareQuery({ sources: ['/:notes.example.dev:class.Note'], limit: 'all' }),
|
|
168
168
|
).toThrow('--limit must be a positive integer')
|
|
169
169
|
expect(() =>
|
|
170
|
-
prepareQuery({
|
|
171
|
-
|
|
170
|
+
prepareQuery({
|
|
171
|
+
sources: [],
|
|
172
|
+
ast: {},
|
|
173
|
+
class: '/:notes.example.dev:class.Note',
|
|
174
|
+
}),
|
|
175
|
+
).toThrow('--ast/--file cannot be combined with sources, --class, --edge, or --direction')
|
|
176
|
+
expect(() => prepareQuery({ sources: [], class: '/:notes.example.dev:view.note' })).toThrow(
|
|
177
|
+
'--class must be one canonical Class Path',
|
|
178
|
+
)
|
|
172
179
|
})
|
|
173
180
|
})
|
package/src/graph/query.ts
CHANGED
|
@@ -12,7 +12,7 @@ import { classReference } from './class'
|
|
|
12
12
|
|
|
13
13
|
export interface QueryCommandInput {
|
|
14
14
|
readonly sources: readonly string[]
|
|
15
|
-
readonly
|
|
15
|
+
readonly class?: string
|
|
16
16
|
readonly ast?: unknown
|
|
17
17
|
readonly edge?: string
|
|
18
18
|
readonly direction?: QueryDirection
|
|
@@ -27,36 +27,34 @@ export interface PreparedQuery {
|
|
|
27
27
|
|
|
28
28
|
const DEFAULT_LIMIT = 100
|
|
29
29
|
|
|
30
|
-
/** Admit canonical Query V6 or author the intentionally small Path/
|
|
30
|
+
/** Admit canonical Query V6 or author the intentionally small Path/Class/one-edge subset. */
|
|
31
31
|
export function prepareQuery(input: QueryCommandInput): PreparedQuery {
|
|
32
32
|
const limit = positiveInteger(input.limit, '--limit', DEFAULT_LIMIT)
|
|
33
33
|
if (input.ast !== undefined) {
|
|
34
34
|
if (
|
|
35
35
|
input.sources.length > 0 ||
|
|
36
|
-
input.
|
|
36
|
+
input.class !== undefined ||
|
|
37
37
|
input.edge !== undefined ||
|
|
38
38
|
input.direction !== undefined
|
|
39
39
|
) {
|
|
40
40
|
throw new TypeError(
|
|
41
|
-
'--ast/--file cannot be combined with sources, --
|
|
41
|
+
'--ast/--file cannot be combined with sources, --class, --edge, or --direction',
|
|
42
42
|
)
|
|
43
43
|
}
|
|
44
44
|
return withPage(QueryAST.decode(input.ast), limit, input.cursor)
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
-
if (input.sources.length === 0 && input.
|
|
48
|
-
throw new TypeError(
|
|
49
|
-
'query requires a Path source, --definition, or a canonical Query V6 document',
|
|
50
|
-
)
|
|
47
|
+
if (input.sources.length === 0 && input.class === undefined) {
|
|
48
|
+
throw new TypeError('query requires a Path source, --class, or a canonical Query V6 document')
|
|
51
49
|
}
|
|
52
50
|
if (input.direction !== undefined && input.edge === undefined) {
|
|
53
51
|
throw new TypeError('--direction requires --edge')
|
|
54
52
|
}
|
|
55
53
|
|
|
56
54
|
const paths = input.sources.map((source) => Path.parse(source))
|
|
57
|
-
const
|
|
58
|
-
input.
|
|
59
|
-
const nodes =
|
|
55
|
+
const selectedClass =
|
|
56
|
+
input.class === undefined ? undefined : classReference(input.class, '--class')
|
|
57
|
+
const nodes = selectedClass === undefined ? paths : [...paths, selectedClass]
|
|
60
58
|
const query: NodeQueryBuilder<unknown> = Query.from({
|
|
61
59
|
nodes: nodes as [QueryNodeInput, ...QueryNodeInput[]],
|
|
62
60
|
})
|
|
@@ -145,6 +145,12 @@ async function installFixture(
|
|
|
145
145
|
) {
|
|
146
146
|
const calls: string[][] = []
|
|
147
147
|
await installer(sourceRoot, snapshot, home, lockPath, calls)('npx', [])
|
|
148
|
+
const lock = await readLock(lockPath)
|
|
149
|
+
for (const skill of snapshot.skills) {
|
|
150
|
+
lock.skills[skill.name].astraleSourceRevision = snapshot.revision
|
|
151
|
+
lock.skills[skill.name].astraleSourceTree = skill.tree
|
|
152
|
+
}
|
|
153
|
+
await writeFile(lockPath, `${JSON.stringify(lock, null, 2)}\n`)
|
|
148
154
|
}
|
|
149
155
|
|
|
150
156
|
describe('Astrale skill reconciliation', () => {
|
|
@@ -164,9 +170,17 @@ describe('Astrale skill reconciliation', () => {
|
|
|
164
170
|
expect(calls).toHaveLength(1)
|
|
165
171
|
expect(calls[0]).toContain(`astrale-os/cli#${source.snapshot.ref}`)
|
|
166
172
|
expect(requestedSkills(calls[0])).toEqual(source.snapshot.skills.map((skill) => skill.name))
|
|
167
|
-
|
|
173
|
+
const installedLock = await readLock(target.lockPath)
|
|
174
|
+
expect(Object.keys(installedLock.skills).sort()).toEqual(
|
|
168
175
|
source.snapshot.skills.map((skill) => skill.name).sort(),
|
|
169
176
|
)
|
|
177
|
+
for (const skill of source.snapshot.skills) {
|
|
178
|
+
expect(installedLock.skills[skill.name]).toMatchObject({
|
|
179
|
+
ref: 'main',
|
|
180
|
+
astraleSourceRevision: source.snapshot.revision,
|
|
181
|
+
astraleSourceTree: skill.tree,
|
|
182
|
+
})
|
|
183
|
+
}
|
|
170
184
|
})
|
|
171
185
|
|
|
172
186
|
test('a coherent older cohort updates every current source skill', async () => {
|
|
@@ -398,7 +412,18 @@ describe('Astrale skill reconciliation', () => {
|
|
|
398
412
|
lockPath: target.lockPath,
|
|
399
413
|
resolveSource: async () => source.snapshot,
|
|
400
414
|
}),
|
|
401
|
-
).toEqual({
|
|
415
|
+
).toEqual({
|
|
416
|
+
status: 'current',
|
|
417
|
+
source: {
|
|
418
|
+
repository: 'astrale-os/cli',
|
|
419
|
+
revision: source.snapshot.revision,
|
|
420
|
+
skills: source.snapshot.skills.map(({ name, tree, path }) => ({
|
|
421
|
+
name,
|
|
422
|
+
tree,
|
|
423
|
+
entrypoint: path,
|
|
424
|
+
})),
|
|
425
|
+
},
|
|
426
|
+
})
|
|
402
427
|
expect(await filesystemSnapshot(target.root)).toBe(beforeCurrentCheck)
|
|
403
428
|
|
|
404
429
|
await rm(join(target.home, '.agents/skills/astrale-cli'), { recursive: true, force: true })
|
package/src/lib/skills/sync.ts
CHANGED
|
@@ -52,6 +52,11 @@ export type SkillApplyStatus =
|
|
|
52
52
|
export type SkillCheckResult = {
|
|
53
53
|
status: SkillCheckStatus
|
|
54
54
|
error?: string
|
|
55
|
+
source?: {
|
|
56
|
+
repository: typeof ASTRALE_CLI_SKILL_SOURCE
|
|
57
|
+
revision: string
|
|
58
|
+
skills: Array<{ name: string; tree: string; entrypoint: string }>
|
|
59
|
+
}
|
|
55
60
|
}
|
|
56
61
|
|
|
57
62
|
export type SkillApplyResult = {
|
|
@@ -70,6 +75,8 @@ type SkillLockEntry = {
|
|
|
70
75
|
skillFolderHash?: string
|
|
71
76
|
installedAt?: string
|
|
72
77
|
updatedAt?: string
|
|
78
|
+
astraleSourceRevision?: string
|
|
79
|
+
astraleSourceTree?: string
|
|
73
80
|
}
|
|
74
81
|
|
|
75
82
|
type SkillLock = {
|
|
@@ -83,6 +90,7 @@ type SkillState = 'absent' | 'current' | 'outdated' | 'unhealthy'
|
|
|
83
90
|
|
|
84
91
|
type SkillInspection = {
|
|
85
92
|
state: SkillState
|
|
93
|
+
sourceCurrent: boolean
|
|
86
94
|
managedNames: string[]
|
|
87
95
|
managedFolders: string[]
|
|
88
96
|
}
|
|
@@ -297,7 +305,7 @@ async function inspectAstraleSkills(
|
|
|
297
305
|
}),
|
|
298
306
|
)
|
|
299
307
|
if (managed.length === 0 && expectedPresence.every((present) => !present)) {
|
|
300
|
-
return { state: 'absent', managedNames: [], managedFolders: [] }
|
|
308
|
+
return { state: 'absent', sourceCurrent: false, managedNames: [], managedFolders: [] }
|
|
301
309
|
}
|
|
302
310
|
|
|
303
311
|
const folders = managed.flatMap(([name, entry]) => {
|
|
@@ -348,7 +356,7 @@ async function inspectAstraleSkills(
|
|
|
348
356
|
}
|
|
349
357
|
}
|
|
350
358
|
|
|
351
|
-
const
|
|
359
|
+
const sourceCurrent =
|
|
352
360
|
coherent &&
|
|
353
361
|
managed.length === snapshot.skills.length &&
|
|
354
362
|
folders.every(({ key, folder, entry }) => {
|
|
@@ -360,9 +368,20 @@ async function inspectAstraleSkills(
|
|
|
360
368
|
actualHashes.get(folder) === skill.tree
|
|
361
369
|
)
|
|
362
370
|
})
|
|
371
|
+
const exactCurrent =
|
|
372
|
+
sourceCurrent &&
|
|
373
|
+
folders.every(({ folder, entry }) => {
|
|
374
|
+
const skill = expected.get(folder)
|
|
375
|
+
return (
|
|
376
|
+
skill !== undefined &&
|
|
377
|
+
entry.astraleSourceRevision === snapshot.revision &&
|
|
378
|
+
entry.astraleSourceTree === skill.tree
|
|
379
|
+
)
|
|
380
|
+
})
|
|
363
381
|
|
|
364
382
|
return {
|
|
365
383
|
state: exactCurrent ? 'current' : coherent ? 'outdated' : 'unhealthy',
|
|
384
|
+
sourceCurrent,
|
|
366
385
|
managedNames: managed.map(([name]) => name),
|
|
367
386
|
managedFolders: [...uniqueFolders],
|
|
368
387
|
}
|
|
@@ -405,6 +424,28 @@ async function installSnapshot(
|
|
|
405
424
|
)
|
|
406
425
|
}
|
|
407
426
|
|
|
427
|
+
async function stampAstraleSource(
|
|
428
|
+
snapshot: AstraleSkillSourceSnapshot,
|
|
429
|
+
lockPath: string,
|
|
430
|
+
): Promise<void> {
|
|
431
|
+
const { lock } = await readSkillLock(lockPath)
|
|
432
|
+
if (!lock) throw new Error('skill installer receipt is unavailable after installation')
|
|
433
|
+
for (const skill of snapshot.skills) {
|
|
434
|
+
const entry = lock.skills[skill.name]
|
|
435
|
+
if (
|
|
436
|
+
!entry ||
|
|
437
|
+
!sourceOwned(entry) ||
|
|
438
|
+
entry.ref !== snapshot.ref ||
|
|
439
|
+
entry.skillPath !== skill.path
|
|
440
|
+
) {
|
|
441
|
+
throw new Error(`skill installer receipt is incomplete for ${skill.name}`)
|
|
442
|
+
}
|
|
443
|
+
entry.astraleSourceRevision = snapshot.revision
|
|
444
|
+
entry.astraleSourceTree = skill.tree
|
|
445
|
+
}
|
|
446
|
+
await writeSkillLock(lockPath, lock)
|
|
447
|
+
}
|
|
448
|
+
|
|
408
449
|
async function selectedAgents(home: string, lockPath: string): Promise<string[]> {
|
|
409
450
|
const { lock } = await readSkillLock(lockPath)
|
|
410
451
|
const agents = new Set(
|
|
@@ -693,13 +734,27 @@ export async function checkAstraleSkills(
|
|
|
693
734
|
dependencies.home,
|
|
694
735
|
dependencies.lockPath,
|
|
695
736
|
)
|
|
737
|
+
const status =
|
|
738
|
+
inspection.state === 'current'
|
|
739
|
+
? 'current'
|
|
740
|
+
: inspection.state === 'unhealthy'
|
|
741
|
+
? 'repair-needed'
|
|
742
|
+
: 'update-available'
|
|
696
743
|
return {
|
|
697
|
-
status
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
744
|
+
status,
|
|
745
|
+
...(status === 'current'
|
|
746
|
+
? {
|
|
747
|
+
source: {
|
|
748
|
+
repository: ASTRALE_CLI_SKILL_SOURCE,
|
|
749
|
+
revision: snapshot.revision,
|
|
750
|
+
skills: snapshot.skills.map(({ name, tree, path }) => ({
|
|
751
|
+
name,
|
|
752
|
+
tree,
|
|
753
|
+
entrypoint: path,
|
|
754
|
+
})),
|
|
755
|
+
},
|
|
756
|
+
}
|
|
757
|
+
: {}),
|
|
703
758
|
}
|
|
704
759
|
} catch (error) {
|
|
705
760
|
return { status: 'unavailable', error: error instanceof Error ? error.message : String(error) }
|
|
@@ -768,6 +823,15 @@ export async function syncAstraleSkills(
|
|
|
768
823
|
dependencies.run,
|
|
769
824
|
knownRetired,
|
|
770
825
|
)
|
|
826
|
+
const installedInspection = await inspectAstraleSkills(
|
|
827
|
+
snapshot,
|
|
828
|
+
dependencies.home,
|
|
829
|
+
dependencies.lockPath,
|
|
830
|
+
)
|
|
831
|
+
if (!installedInspection.sourceCurrent) {
|
|
832
|
+
throw new Error('installed Astrale skills do not match the resolved source')
|
|
833
|
+
}
|
|
834
|
+
await stampAstraleSource(snapshot, dependencies.lockPath)
|
|
771
835
|
const verified = await inspectAstraleSkills(
|
|
772
836
|
snapshot,
|
|
773
837
|
dependencies.home,
|
|
@@ -195,7 +195,7 @@ describe('program composition', () => {
|
|
|
195
195
|
'whoami',
|
|
196
196
|
])
|
|
197
197
|
expect(createHash('sha256').update(JSON.stringify(surface)).digest('hex')).toBe(
|
|
198
|
-
'
|
|
198
|
+
'91c35b18985ad8606b5f2429fd8860b020a93edccb30dfbaa74d501929a8513b',
|
|
199
199
|
)
|
|
200
200
|
})
|
|
201
201
|
|
|
@@ -464,13 +464,50 @@ describe('help contract — read command split', () => {
|
|
|
464
464
|
expect(queryHelp).not.toContain('--children <json>')
|
|
465
465
|
expect(queryHelp).not.toContain('--edges <json>')
|
|
466
466
|
expect(queryHelp).toContain('--ast <json>')
|
|
467
|
-
expect(queryHelp).toContain('--
|
|
467
|
+
expect(queryHelp).toContain('--class <path>')
|
|
468
|
+
expect(queryHelp).not.toContain('--definition')
|
|
468
469
|
expect(queryHelp).toContain('--edge <class>')
|
|
469
470
|
expect(queryHelp).toContain('--direction <direction>')
|
|
470
471
|
expect(queryHelp).toContain('--limit <n>')
|
|
471
472
|
expect(queryHelp).toContain('--cursor <token>')
|
|
472
473
|
expect(queryHelp).not.toContain('--cypher <query>')
|
|
473
474
|
})
|
|
475
|
+
|
|
476
|
+
test('keeps the removed query --definition flag outside the parsed command surface', async () => {
|
|
477
|
+
const program = await buildProgram()
|
|
478
|
+
const query = allCommands(program).find((command) => command.name() === 'query')
|
|
479
|
+
const parsed = query?.parseOptions(['--definition', '/:notes.example.dev:class.Note'])
|
|
480
|
+
|
|
481
|
+
expect(parsed?.unknown).toContain('--definition')
|
|
482
|
+
expect(query?.options.some((option) => option.attributeName() === 'definition')).toBe(false)
|
|
483
|
+
})
|
|
484
|
+
|
|
485
|
+
test('routes query --class through Commander as the exact Class option', async () => {
|
|
486
|
+
const program = await buildProgram()
|
|
487
|
+
const query = allCommands(program).find((command) => command.name() === 'query')
|
|
488
|
+
let observed: unknown
|
|
489
|
+
query?.action((sources, options) => {
|
|
490
|
+
observed = { sources, class: options.class, limit: options.limit, json: options.json }
|
|
491
|
+
})
|
|
492
|
+
|
|
493
|
+
await program.parseAsync([
|
|
494
|
+
'node',
|
|
495
|
+
'astrale',
|
|
496
|
+
'query',
|
|
497
|
+
'--class',
|
|
498
|
+
'/:notes.example.dev:class.Note',
|
|
499
|
+
'--limit',
|
|
500
|
+
'1',
|
|
501
|
+
'--json',
|
|
502
|
+
])
|
|
503
|
+
|
|
504
|
+
expect(observed).toEqual({
|
|
505
|
+
sources: [],
|
|
506
|
+
class: '/:notes.example.dev:class.Note',
|
|
507
|
+
limit: '1',
|
|
508
|
+
json: true,
|
|
509
|
+
})
|
|
510
|
+
})
|
|
474
511
|
})
|
|
475
512
|
|
|
476
513
|
describe('help contract — explicit anonymous authentication', () => {
|
|
@@ -45,7 +45,7 @@ test('machine runner uses the exact descriptor, appends json mode, and versions
|
|
|
45
45
|
})
|
|
46
46
|
|
|
47
47
|
const result = await runStudioCliJson(
|
|
48
|
-
['query', '--
|
|
48
|
+
['query', '--class', '/:example.dev:class.Issue'],
|
|
49
49
|
decodeJsonObject,
|
|
50
50
|
)
|
|
51
51
|
|
|
@@ -55,7 +55,7 @@ test('machine runner uses the exact descriptor, appends json mode, and versions
|
|
|
55
55
|
exitCode: 0,
|
|
56
56
|
timedOut: false,
|
|
57
57
|
data: {
|
|
58
|
-
command: ['query', '--
|
|
58
|
+
command: ['query', '--class', '/:example.dev:class.Issue', '--json'],
|
|
59
59
|
},
|
|
60
60
|
})
|
|
61
61
|
})
|