@astrale-os/cli 0.8.1-alpha.4 → 0.8.1-alpha.6

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.
Files changed (56) hide show
  1. package/README.md +1 -1
  2. package/dist/astrale.js +827 -575
  3. package/dist/public/connect-core.js +22 -16
  4. package/dist/public/keys/index.js +3 -10
  5. package/dist/public/paths/index.js +3 -10
  6. package/dist/types/lib/instance-target.d.ts +2 -0
  7. package/dist/types/lib/invocation.d.ts +3 -0
  8. package/dist/types/lib/log.d.ts +6 -5
  9. package/dist/types/lib/output.d.ts +6 -2
  10. package/package.json +1 -1
  11. package/src/commands/__tests__/introspect-parse.test.ts +21 -0
  12. package/src/commands/__tests__/logs.test.ts +5 -0
  13. package/src/commands/__tests__/read-commands.test.ts +11 -1
  14. package/src/commands/__tests__/token-ttl.test.ts +21 -0
  15. package/src/commands/auth/token.ts +19 -7
  16. package/src/commands/call.ts +18 -43
  17. package/src/commands/get.ts +23 -5
  18. package/src/commands/identity/create.ts +11 -2
  19. package/src/commands/identity/delete.ts +8 -2
  20. package/src/commands/identity/export.ts +8 -2
  21. package/src/commands/identity/import.ts +11 -2
  22. package/src/commands/identity/sync.ts +8 -2
  23. package/src/commands/identity/unsync.ts +12 -2
  24. package/src/commands/identity/use.ts +8 -2
  25. package/src/commands/identity/whoami.ts +1 -1
  26. package/src/commands/introspect.ts +117 -0
  27. package/src/commands/logs.ts +50 -6
  28. package/src/commands/mutate.ts +2 -3
  29. package/src/commands/query.ts +3 -5
  30. package/src/commands/token.ts +35 -8
  31. package/src/commands/update.ts +30 -9
  32. package/src/commands/view.ts +5 -3
  33. package/src/connection/__tests__/credential.test.ts +10 -0
  34. package/src/connection/__tests__/errors.test.ts +20 -0
  35. package/src/connection/credential.ts +3 -0
  36. package/src/connection/errors.ts +74 -12
  37. package/src/connection/session.ts +9 -0
  38. package/src/connection/target.ts +1 -0
  39. package/src/graph/__tests__/mutation.test.ts +6 -0
  40. package/src/graph/mutation.ts +13 -0
  41. package/src/identity/__tests__/registry.test.ts +1 -1
  42. package/src/identity/registry.ts +12 -5
  43. package/src/lib/__tests__/command-dx.test.ts +47 -15
  44. package/src/lib/__tests__/instance-target.test.ts +17 -0
  45. package/src/lib/__tests__/output.test.ts +12 -0
  46. package/src/lib/command-dx.ts +41 -23
  47. package/src/lib/instance-target.ts +18 -2
  48. package/src/lib/invocation.ts +11 -0
  49. package/src/lib/log.ts +17 -9
  50. package/src/lib/output.ts +20 -4
  51. package/src/program/__tests__/program.test.ts +4 -2
  52. package/src/program/build.ts +3 -9
  53. package/src/program/options.ts +2 -1
  54. package/src/state/__tests__/identities.test.ts +2 -2
  55. package/src/state/identities.ts +3 -10
  56. package/src/commands/describe.ts +0 -86
@@ -52281,17 +52281,10 @@ async function readDecoded(options) {
52281
52281
  function hasVersion(input) {
52282
52282
  return typeof input === "object" && input !== null && Object.hasOwn(input, "version");
52283
52283
  }
52284
- function seed(now) {
52284
+ function seed(_now) {
52285
52285
  return {
52286
- default: "manager",
52287
- identities: {
52288
- manager: {
52289
- subject: "manager",
52290
- createdAt: now.toISOString(),
52291
- source: "key",
52292
- mode: "local"
52293
- }
52294
- }
52286
+ default: "",
52287
+ identities: {}
52295
52288
  };
52296
52289
  }
52297
52290
  function encodeV1(store) {
@@ -54096,10 +54089,10 @@ async function readIdentities() {
54096
54089
  }
54097
54090
  async function getDefault() {
54098
54091
  const store = await readIdentities();
54099
- const identity3 = store.identities[store.default];
54100
- if (!identity3) {
54101
- throw new Error(`Default identity "${store.default}" not found. Run: astrale identity create ${store.default}`);
54092
+ if (store.default === "" || store.identities[store.default] === undefined) {
54093
+ throw new AstraleError("NO_IDENTITY", "No default identity.", "Run: astrale identity create <name>");
54102
54094
  }
54095
+ const identity3 = store.identities[store.default];
54103
54096
  return { ...identity3, name: store.default };
54104
54097
  }
54105
54098
  async function getIdentity(name) {
@@ -54492,9 +54485,9 @@ async function resolveNamedInstanceTarget(identifier, opts) {
54492
54485
  try {
54493
54486
  managed = await opts.managed(identifier);
54494
54487
  } catch (e) {
54495
- if (!isManagedInstanceNotFound(e))
54496
- throw e;
54497
- throw notFound;
54488
+ if (isManagedInstanceNotFound(e) || isAdminDiscoveryFailure(e))
54489
+ throw notFound;
54490
+ throw e;
54498
54491
  }
54499
54492
  const url3 = normalizeInstanceKernelUrl(managed.url);
54500
54493
  return {
@@ -54560,6 +54553,19 @@ function adminTargetToInstance(target) {
54560
54553
  ...target.caFile ? { caFile: target.caFile } : {}
54561
54554
  };
54562
54555
  }
54556
+ var ADMIN_DISCOVERY_CODES = new Set([
54557
+ "TOKEN_EXCHANGE_SOURCE_INVALID",
54558
+ "TOKEN_EXCHANGE_SOURCE_EXPIRED",
54559
+ "TOKEN_EXCHANGE_UNSUPPORTED",
54560
+ "TOKEN_EXCHANGE_DISCOVERY_FAILED",
54561
+ "TOKEN_EXCHANGE_PROTOCOL_ERROR",
54562
+ "TOKEN_EXCHANGE_INSECURE",
54563
+ "ADMIN_DOMAIN_ISSUER_MISSING",
54564
+ "ADMIN_INVENTORY_UNAVAILABLE"
54565
+ ]);
54566
+ function isAdminDiscoveryFailure(error51) {
54567
+ return error51 instanceof AstraleError && ADMIN_DISCOVERY_CODES.has(error51.code);
54568
+ }
54563
54569
  function isManagedInstanceNotFound(error51) {
54564
54570
  if (error51 instanceof AstraleError && error51.code === "INSTANCE_NOT_FOUND")
54565
54571
  return true;
@@ -45340,17 +45340,10 @@ async function readDecoded(options) {
45340
45340
  function hasVersion(input) {
45341
45341
  return typeof input === "object" && input !== null && Object.hasOwn(input, "version");
45342
45342
  }
45343
- function seed(now) {
45343
+ function seed(_now) {
45344
45344
  return {
45345
- default: "manager",
45346
- identities: {
45347
- manager: {
45348
- subject: "manager",
45349
- createdAt: now.toISOString(),
45350
- source: "key",
45351
- mode: "local"
45352
- }
45353
- }
45345
+ default: "",
45346
+ identities: {}
45354
45347
  };
45355
45348
  }
45356
45349
  function encodeV1(store) {
@@ -43940,17 +43940,10 @@ async function readDecoded(options) {
43940
43940
  function hasVersion(input) {
43941
43941
  return typeof input === "object" && input !== null && Object.hasOwn(input, "version");
43942
43942
  }
43943
- function seed(now) {
43943
+ function seed(_now) {
43944
43944
  return {
43945
- default: "manager",
43946
- identities: {
43947
- manager: {
43948
- subject: "manager",
43949
- createdAt: now.toISOString(),
43950
- source: "key",
43951
- mode: "local"
43952
- }
43953
- }
43945
+ default: "",
43946
+ identities: {}
43954
43947
  };
43955
43948
  }
43956
43949
  function encodeV1(store) {
@@ -33,4 +33,6 @@ export type ResolveInstanceTargetOpts = {
33
33
  export declare function resolveInstanceTarget(request: InstanceTargetRequest, opts: ResolveInstanceTargetOpts): Promise<ResolvedInstanceTarget>;
34
34
  export declare function couldBeConfiguredAdminInstance(identifier: string, config: AstraleConfig): boolean;
35
35
  export declare function adminTargetToInstance(target: ResolvedAdminTarget): ResolvedInstanceTarget;
36
+ /** Admin lookup failed before it could say whether the slug exists. */
37
+ export declare function isAdminDiscoveryFailure(error: unknown): boolean;
36
38
  export declare function isManagedInstanceNotFound(error: unknown): boolean;
@@ -0,0 +1,3 @@
1
+ /** Process-wide machine mode from argv (`--json` / `--raw` / `--ci`). */
2
+ export declare function configureInvocation(argv: readonly string[]): void;
3
+ export declare function invocationWantsMachine(): boolean;
@@ -1,5 +1,5 @@
1
1
  import { type Ora } from 'ora';
2
- import { type RawOutputOpts } from './output';
2
+ import { type MachineOpts } from './output';
3
3
  export declare const log: {
4
4
  info: (msg: string) => void;
5
5
  success: (msg: string) => void;
@@ -8,10 +8,11 @@ export declare const log: {
8
8
  step: (msg: string) => void;
9
9
  dim: (msg: string) => void;
10
10
  };
11
- /** Report an error with hint (when present) and exit. Commands that carry
12
- * RawOutputOpts should pass them so machine consumers (--json/--raw/piped)
13
- * get one structured JSON line on stderr instead of the pretty ✖ view. */
14
- export declare function fatal(e: unknown, opts?: RawOutputOpts): never;
11
+ /** Report an error with hint (when present) and exit. `--json` / `--ci` / a
12
+ * non-TTY stdout always get one structured JSON line on stderr. */
13
+ export declare function fatal(e: unknown, opts?: MachineOpts): never;
14
+ /** Admit expected invalid input and exit through {@link fatal}. */
15
+ export declare function failClosed(error: unknown, opts?: MachineOpts): never;
15
16
  /** Shortcut for stub commands that aren't wired in v1 (§15). */
16
17
  export declare function fatalNotImplemented(feature: string, hint?: string): never;
17
18
  /**
@@ -6,6 +6,9 @@ export type OutputOpts = {
6
6
  format?: 'yaml' | 'json';
7
7
  };
8
8
  export type RawOutputOpts = Pick<OutputOpts, 'raw' | 'json'>;
9
+ export type MachineOpts = RawOutputOpts & {
10
+ readonly ci?: boolean;
11
+ };
9
12
  export declare const RAW_OUTPUT_OPTIONS: readonly [{
10
13
  readonly flags: '--json';
11
14
  readonly description: 'Always-valid JSON (for jq)';
@@ -15,9 +18,10 @@ export declare const RAW_OUTPUT_OPTIONS: readonly [{
15
18
  }];
16
19
  /**
17
20
  * Is the consumer a machine (emit structured data, not a pretty view)?
18
- * True for `--json`, `--raw`, or any non-TTY stdout (pipe, redirect, CI, agent).
21
+ * True for `--json`, `--raw`, `--ci`, a process-wide `--ci/--json/--raw` on argv,
22
+ * or any non-TTY stdout (pipe, redirect, agent).
19
23
  */
20
- export declare function isMachine(opts?: RawOutputOpts): boolean;
24
+ export declare function isMachine(opts?: MachineOpts): boolean;
21
25
  /**
22
26
  * `--raw` = the *unwrapped* value (bare scalar, raw bytes). The raw-vs-json
23
27
  * distinction only manifests for scalars and binary; objects/arrays fall back
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@astrale-os/cli",
3
- "version": "0.8.1-alpha.4",
3
+ "version": "0.8.1-alpha.6",
4
4
  "description": "Astrale CLI — connect to existing Astrale kernels",
5
5
  "keywords": [
6
6
  "astrale",
@@ -0,0 +1,21 @@
1
+ import { describe, expect, test } from 'bun:test'
2
+
3
+ import { parseIntrospectTarget } from '../introspect'
4
+
5
+ describe('parseIntrospectTarget', () => {
6
+ test('accepts a bare origin', () => {
7
+ const parsed = parseIntrospectTarget('host.astrale.ai')
8
+ expect(parsed.origin).toBe('host.astrale.ai')
9
+ expect(parsed.path.ast.steps).toEqual([])
10
+ })
11
+
12
+ test('accepts a Domain-rooted method Path', () => {
13
+ const parsed = parseIntrospectTarget('/:host.astrale.ai:class.Manager:createInstance')
14
+ expect(parsed.origin).toBe('host.astrale.ai')
15
+ expect(parsed.path.ast.steps.at(-1)?.kind).toBe('method')
16
+ })
17
+
18
+ test('rejects an @id', () => {
19
+ expect(() => parseIntrospectTarget('@abc')).toThrow('not an @id')
20
+ })
21
+ })
@@ -29,6 +29,11 @@ describe('buildJournalInput', () => {
29
29
  expect(buildJournalInput({})).toEqual({ limit: 200 })
30
30
  expect(() => buildJournalInput({ limit: '0' })).toThrow('--limit')
31
31
  expect(() => buildJournalInput({ limit: 'all' })).toThrow('--limit')
32
+ expect(() => buildJournalInput({ since: 'not-a-date' })).toThrow('--since')
33
+ expect(() =>
34
+ buildJournalInput({ since: '2026-01-01T00:00:00Z', until: '1999-01-01T00:00:00Z' }),
35
+ ).toThrow('--since')
36
+ expect(() => buildJournalInput({ cursor: 'junk' })).toThrow('--cursor')
32
37
  })
33
38
  })
34
39
 
@@ -27,6 +27,7 @@ class ExitError extends Error {
27
27
  let stdout = ''
28
28
  let errors: string[] = []
29
29
  let originalStdoutWrite: typeof process.stdout.write
30
+ let originalStderrWrite: typeof process.stderr.write
30
31
  let originalConsoleError: typeof console.error
31
32
  let originalExit: typeof process.exit
32
33
  let queryCalls: Array<{ ast: unknown; options: unknown }> = []
@@ -95,12 +96,17 @@ beforeEach(() => {
95
96
  runKernelCommandMock.mockClear()
96
97
 
97
98
  originalStdoutWrite = process.stdout.write
99
+ originalStderrWrite = process.stderr.write
98
100
  originalConsoleError = console.error
99
101
  originalExit = process.exit
100
102
  process.stdout.write = ((chunk: string | Uint8Array) => {
101
103
  stdout += typeof chunk === 'string' ? chunk : Buffer.from(chunk).toString('utf8')
102
104
  return true
103
105
  }) as typeof process.stdout.write
106
+ process.stderr.write = ((chunk: string | Uint8Array) => {
107
+ errors.push(typeof chunk === 'string' ? chunk : Buffer.from(chunk).toString('utf8'))
108
+ return true
109
+ }) as typeof process.stderr.write
104
110
  console.error = ((...values: unknown[]) => {
105
111
  errors.push(values.map(String).join(' '))
106
112
  }) as typeof console.error
@@ -111,6 +117,7 @@ beforeEach(() => {
111
117
 
112
118
  afterEach(() => {
113
119
  process.stdout.write = originalStdoutWrite
120
+ process.stderr.write = originalStderrWrite
114
121
  console.error = originalConsoleError
115
122
  process.exit = originalExit
116
123
  })
@@ -195,7 +202,10 @@ describe('query command', () => {
195
202
  ).rejects.toEqual(new ExitError(1))
196
203
 
197
204
  expect(runKernelCommandMock).not.toHaveBeenCalled()
198
- expect(errors.join('\n')).toContain('expected "astrale.graph.query" at /format')
205
+ expect(JSON.parse(errors.join('\n'))).toMatchObject({
206
+ error: 'INVALID_INPUT',
207
+ message: 'Invalid input: expected "astrale.graph.query" at /format.',
208
+ })
199
209
  })
200
210
  })
201
211
 
@@ -0,0 +1,21 @@
1
+ import { describe, expect, test } from 'bun:test'
2
+
3
+ import { AstraleError } from '../../errors'
4
+ import { parseTtl } from '../token'
5
+
6
+ describe('parseTtl', () => {
7
+ test('defaults to 3600 seconds', () => {
8
+ expect(parseTtl(undefined)).toBe(3600)
9
+ })
10
+
11
+ test('rejects non-positive and non-integer values', () => {
12
+ expect(() => parseTtl('abc')).toThrow(AstraleError)
13
+ expect(() => parseTtl('0')).toThrow(AstraleError)
14
+ expect(() => parseTtl('-5')).toThrow(AstraleError)
15
+ expect(() => parseTtl('1.5')).toThrow(AstraleError)
16
+ })
17
+
18
+ test('admits a positive integer', () => {
19
+ expect(parseTtl('90')).toBe(90)
20
+ })
21
+ })
@@ -1,9 +1,10 @@
1
1
  import type { CommandDefinition } from '../../program/index'
2
2
 
3
+ import { AstraleError } from '../../errors'
3
4
  import { getDefault, getIdentity, readIdentities } from '../../identity/index'
4
5
  import { isSessionExpired, readIdpSession, type IdpSession } from '../../lib/idp'
5
6
  import { ensureFreshSession } from '../../lib/idp-session'
6
- import { log } from '../../lib/log'
7
+ import { fatal, log } from '../../lib/log'
7
8
  import { output } from '../../lib/output'
8
9
 
9
10
  type AuthTokenType = 'access' | 'id'
@@ -66,7 +67,12 @@ Examples:
66
67
  $ astrale auth token --name alice --type id --json
67
68
  `,
68
69
  action: async (opts: AuthTokenOpts) => {
69
- const result = await resolveAuthToken(opts)
70
+ let result: AuthTokenResult
71
+ try {
72
+ result = await resolveAuthToken(opts)
73
+ } catch (error) {
74
+ fatal(error, opts)
75
+ }
70
76
 
71
77
  if (opts.raw) {
72
78
  process.stdout.write(result.token + '\n')
@@ -118,10 +124,11 @@ async function resolveIdentityName(opts: AuthTokenOpts): Promise<string> {
118
124
  if (opts.name) {
119
125
  const identity = await getIdentity(opts.name)
120
126
  if ((identity.source ?? 'key') !== 'idp') {
121
- throw new Error(`Identity "${opts.name}" is not IdP-backed`)
127
+ throw new AstraleError('AUTH_ERROR', `Identity "${opts.name}" is not IdP-backed`)
122
128
  }
123
129
  if (opts.idp && identity.idp !== opts.idp) {
124
- throw new Error(
130
+ throw new AstraleError(
131
+ 'AUTH_ERROR',
125
132
  `Identity "${opts.name}" is backed by IdP "${identity.idp ?? '?'}", not "${opts.idp}"`,
126
133
  )
127
134
  }
@@ -135,20 +142,25 @@ async function resolveIdentityName(opts: AuthTokenOpts): Promise<string> {
135
142
  .map(([name]) => name)
136
143
 
137
144
  if (matches.length === 0) {
138
- throw new Error(
145
+ throw new AstraleError(
146
+ 'AUTH_ERROR',
139
147
  `No IdP-backed identities found for IdP "${opts.idp}". Run: astrale auth login --idp ${opts.idp}`,
140
148
  )
141
149
  }
142
150
  if (matches.includes(store.default)) return store.default
143
151
  if (matches.length === 1) return matches[0]
144
- throw new Error(
152
+ throw new AstraleError(
153
+ 'AUTH_ERROR',
145
154
  `Multiple IdP-backed identities found for IdP "${opts.idp}": ${matches.join(', ')}. Pass --name.`,
146
155
  )
147
156
  }
148
157
 
149
158
  const identity = await getDefault()
150
159
  if ((identity.source ?? 'key') !== 'idp') {
151
- throw new Error('Default identity is not IdP-backed. Pass --name or --idp.')
160
+ throw new AstraleError(
161
+ 'AUTH_ERROR',
162
+ 'Default identity is not IdP-backed. Pass --name or --idp.',
163
+ )
152
164
  }
153
165
  return identity.name
154
166
  }
@@ -4,15 +4,12 @@ import type { ConnectionContext, KernelCommandOpts } from '../connection'
4
4
  import type { CommandDefinition } from '../program/index'
5
5
 
6
6
  import { createPathCall, expandSelfInCall, runKernelCommand, withSelfHint } from '../connection'
7
- import { nodeProperty } from '../graph/index'
8
7
  import { presentBinary } from '../lib/binary'
9
- import { log } from '../lib/log'
8
+ import { failClosed, log } from '../lib/log'
10
9
  import { output, present } from '../lib/output'
11
- import { describeCallableFromSchema, missingCallableDescription } from './call-describe'
12
10
 
13
11
  type CallOpts = KernelCommandOpts & {
14
12
  data?: string
15
- describe?: boolean
16
13
  dryRun?: boolean
17
14
  output?: string
18
15
  }
@@ -32,15 +29,13 @@ export async function callCommand(
32
29
  try {
33
30
  expanded = await expandSelfInCall(path, rawParams, opts)
34
31
  } catch (error) {
35
- log.error(error instanceof Error ? error.message : 'Invalid @self expansion')
36
- process.exit(1)
32
+ failClosed(error, opts)
37
33
  }
38
34
  const expandedPath = expanded.path
39
-
40
- // ── Describe mode: show schema without executing ────────
41
- // Runs AFTER expansion so `astrale call @self::m --describe` works.
42
- if (opts.describe) {
43
- return describeOperation(expandedPath, opts)
35
+ try {
36
+ Path.parse(expandedPath)
37
+ } catch (error) {
38
+ failClosed(error, opts)
44
39
  }
45
40
 
46
41
  // ── Parse params ────────────────────────────────────────
@@ -48,8 +43,7 @@ export async function callCommand(
48
43
  try {
49
44
  params = await parseParams([...expanded.parameters], opts.data)
50
45
  } catch (e) {
51
- log.error(e instanceof Error ? e.message : 'Invalid params')
52
- process.exit(1)
46
+ failClosed(e, opts)
53
47
  }
54
48
 
55
49
  // ── Dry-run: show what would be sent ─────────────────────
@@ -94,24 +88,6 @@ export async function materializeCallResult(result: CallResult): Promise<Materia
94
88
  return Object.freeze({ kind: 'stream', values: Object.freeze(values) })
95
89
  }
96
90
 
97
- async function describeOperation(path: string, opts: CallOpts): Promise<void> {
98
- await runKernelCommand({
99
- opts,
100
- label: `Schema for ${path}`,
101
- fn: async ({ graph }) => {
102
- const parsed = Path.parse(path)
103
- if (parsed.ast.anchor.kind !== 'domain') {
104
- throw missingCallableDescription(path)
105
- }
106
- const domain = await graph.getOrThrow(Path.parse(`/:${parsed.ast.anchor.origin}`))
107
- const described = describeCallableFromSchema(parsed, nodeProperty(domain, 'schema'))
108
- if (described === undefined) throw missingCallableDescription(path)
109
- return described
110
- },
111
- format: (schema, fmtOpts) => output(schema, fmtOpts),
112
- })
113
- }
114
-
115
91
  // ── Param parsing ───────────────────────────────────────────
116
92
 
117
93
  export async function parseParams(
@@ -129,6 +105,10 @@ export async function parseParams(
129
105
  }
130
106
  }
131
107
 
108
+ if (rawParams.length > 0) {
109
+ return parseKeyValue(rawParams)
110
+ }
111
+
132
112
  const stdin = await readStdin()
133
113
  if (stdin) {
134
114
  try {
@@ -138,10 +118,6 @@ export async function parseParams(
138
118
  }
139
119
  }
140
120
 
141
- if (rawParams.length > 0) {
142
- return parseKeyValue(rawParams)
143
- }
144
-
145
121
  return {}
146
122
  }
147
123
 
@@ -201,11 +177,12 @@ export default {
201
177
  description: 'Call a kernel operation',
202
178
  afterHelpText: `
203
179
  Behavior:
204
- Param priority (highest wins): --data > stdin > key=value > {}. If
180
+ Param priority (highest wins): --data > key=value > stdin > {}. If
205
181
  both --data and key=value are given, key=value is ignored (warned).
206
- Stdin is read only when piped (ignored on a TTY). --describe and
207
- --dry-run short-circuit (no execution). Remote-bound functions
208
- auto-mint a worker-scoped credential; --creds overrides it.
182
+ Stdin is read only when piped and no --data/key=value is present
183
+ (ignored on a TTY). --dry-run admits the Path and prints the call
184
+ input without executing. Remote-bound functions auto-mint a
185
+ worker-scoped credential; --creds overrides it.
209
186
 
210
187
  Self-reference:
211
188
  @self expands to your nodeId on the active instance (path head or
@@ -214,11 +191,10 @@ Self-reference:
214
191
  (e.g. via 'astrale get @self --json'). Resolution authenticates to
215
192
  the selected Kernel and never trusts a local registration or JWT sub.
216
193
 
217
- --describe reads the callable's input/output from the installed Domain
218
- schema. Method Paths are not Function nodes.
194
+ Callable input/output lives on astrale introspect <path>.
219
195
 
220
196
  Examples:
221
- $ astrale call /:host.astrale.ai:class.Manager:createInstance --describe
197
+ $ astrale introspect /:host.astrale.ai:class.Manager:createInstance
222
198
  $ astrale call /:blog.acme.com:class.Author:list limit=10
223
199
  $ astrale call '@self::deactivate'
224
200
  $ astrale call /:kernel.astrale.ai:function.journal --data '{"limit":5}' --json
@@ -234,7 +210,6 @@ Examples:
234
210
  options: [
235
211
  { flags: '-d, --data <json>', description: 'Params as JSON string' },
236
212
  { flags: '-o, --output <file>', description: 'Write binary/raw output to a file' },
237
- { flags: '--describe', description: 'Show operation schema without executing' },
238
213
  { flags: '--dry-run', description: 'Show what would be sent without executing' },
239
214
  ],
240
215
  action: async (path, params, opts) => {
@@ -5,26 +5,40 @@ import type { CommandDefinition } from '../program/index'
5
5
 
6
6
  import { expandSelfInPath, runKernelCommand, withSelfHint } from '../connection'
7
7
  import { formatKernelError } from '../connection/errors'
8
- import { isMachine, output } from '../lib/output'
8
+ import { AstraleError } from '../errors'
9
+ import { denoise, isMachine, output } from '../lib/output'
9
10
 
10
- type GetOpts = KernelCommandOpts
11
+ type GetOpts = KernelCommandOpts & { schema?: boolean }
11
12
 
12
13
  /** Read one exact canonical Node. The caller's Path is not fabricated into the Node value. */
13
14
  export async function getCommand(target: string, opts: GetOpts): Promise<void> {
14
15
  let path: string
15
16
  let meta
17
+ let parsed
16
18
  try {
17
19
  ;({ path, meta } = await expandSelfInPath(target, opts))
20
+ parsed = Path.parse(path)
18
21
  } catch (error) {
19
22
  await formatKernelError(error, isMachine(opts), undefined, opts.debug)
20
23
  process.exit(1)
21
24
  }
22
25
 
26
+ const last = parsed.ast.steps.at(-1)
27
+ if (last?.kind === 'method') {
28
+ const error = new AstraleError(
29
+ 'NOT_A_NODE',
30
+ `${path} is a callable Path, not a Node.`,
31
+ `Use \`astrale call ${path}\` or \`astrale introspect ${path}\`.`,
32
+ )
33
+ await formatKernelError(error, isMachine(opts), undefined, opts.debug)
34
+ process.exit(1)
35
+ }
36
+
23
37
  await runKernelCommand({
24
38
  opts,
25
39
  label: `Node ${path}`,
26
- fn: ({ graph }) => withSelfHint(() => graph.getOrThrow(Path.parse(path)), meta),
27
- format: (node, format) => output(node, format),
40
+ fn: ({ graph }) => withSelfHint(() => graph.getOrThrow(parsed), meta),
41
+ format: (node, format) => output(opts.schema === true ? node : denoise(node), format),
28
42
  })
29
43
  }
30
44
 
@@ -38,14 +52,18 @@ Behavior:
38
52
  intentionally indistinguishable. @self is expanded before dispatch.
39
53
 
40
54
  A Node does not contain a synthetic path, labels, or backend class ID.
41
- Use astrale query for graph-shaped reads.
55
+ Method Paths are not Nodes — use call / introspect. Schema-valued
56
+ properties are omitted unless --schema is passed. Use query for
57
+ graph-shaped reads.
42
58
 
43
59
  Examples:
44
60
  $ astrale get /:notes.example.dev:class.Note
45
61
  $ astrale get @abc123 --json
46
62
  $ astrale get @self
63
+ $ astrale get /:host.astrale.ai --schema
47
64
  `,
48
65
  arguments: [{ name: 'target', description: 'Canonical Node Path or @id' }],
66
+ options: [{ flags: '--schema', description: 'Include schema-valued properties' }],
49
67
  action: async (target, opts) => {
50
68
  await getCommand(target as string, opts as GetOpts)
51
69
  },
@@ -2,6 +2,7 @@ import type { CommandDefinition } from '../../program/index'
2
2
 
3
3
  import { createIdentity } from '../../identity/index'
4
4
  import { fatal, log } from '../../lib/log'
5
+ import { isMachine, output, RAW_OUTPUT_OPTIONS, type RawOutputOpts } from '../../lib/output'
5
6
 
6
7
  export default {
7
8
  name: 'create',
@@ -11,16 +12,24 @@ export default {
11
12
  { flags: '--subject <sub>', description: 'Custom subject (defaults to name)' },
12
13
  { flags: '--local', description: 'Local-only identity (default)' },
13
14
  { flags: '--remote', description: 'Remote (cloud-synced) identity — requires cloud login' },
15
+ ...RAW_OUTPUT_OPTIONS,
14
16
  ],
15
- action: async (name: string, opts: { subject?: string; local?: boolean; remote?: boolean }) => {
17
+ action: async (
18
+ name: string,
19
+ opts: { subject?: string; local?: boolean; remote?: boolean } & RawOutputOpts,
20
+ ) => {
16
21
  try {
17
22
  const mode = opts.remote ? 'remote' : 'local'
18
23
  const identity = await createIdentity(name, { subject: opts.subject, mode })
24
+ if (isMachine(opts)) {
25
+ output({ name, ...identity }, opts)
26
+ return
27
+ }
19
28
  log.success(
20
29
  `Created identity "${name}" (subject: ${identity.subject}, mode: ${identity.mode})`,
21
30
  )
22
31
  } catch (e) {
23
- fatal(e)
32
+ fatal(e, opts)
24
33
  }
25
34
  },
26
35
  } satisfies CommandDefinition
@@ -2,17 +2,23 @@ import type { CommandDefinition } from '../../program/index'
2
2
 
3
3
  import { deleteIdentity } from '../../identity/index'
4
4
  import { fatal, log } from '../../lib/log'
5
+ import { isMachine, output, RAW_OUTPUT_OPTIONS, type RawOutputOpts } from '../../lib/output'
5
6
 
6
7
  export default {
7
8
  name: 'delete',
8
9
  description: 'Delete an identity',
9
10
  arguments: [{ name: 'name', description: 'Identity name', required: true }],
10
- action: async (name: string) => {
11
+ options: [...RAW_OUTPUT_OPTIONS],
12
+ action: async (name: string, opts: RawOutputOpts) => {
11
13
  try {
12
14
  await deleteIdentity(name)
15
+ if (isMachine(opts)) {
16
+ output({ deleted: name }, opts)
17
+ return
18
+ }
13
19
  log.success(`Deleted identity "${name}"`)
14
20
  } catch (e) {
15
- fatal(e)
21
+ fatal(e, opts)
16
22
  }
17
23
  },
18
24
  } satisfies CommandDefinition
@@ -2,6 +2,7 @@ import type { CommandDefinition } from '../../program/index'
2
2
 
3
3
  import { encodeIdentityExport, exportIdentity, writeIdentityExport } from '../../identity/index'
4
4
  import { fatal, log } from '../../lib/log'
5
+ import { isMachine, output, RAW_OUTPUT_OPTIONS, type RawOutputOpts } from '../../lib/output'
5
6
  import { readPassphrase } from '../../lib/prompt'
6
7
 
7
8
  /**
@@ -18,8 +19,9 @@ export default {
18
19
  ],
19
20
  options: [
20
21
  { flags: '--encrypt', description: 'Encrypt the envelope with a passphrase (JOSE JWE)' },
22
+ ...RAW_OUTPUT_OPTIONS,
21
23
  ],
22
- action: async (name: string, path: string, opts: { encrypt?: boolean }) => {
24
+ action: async (name: string, path: string, opts: { encrypt?: boolean } & RawOutputOpts) => {
23
25
  try {
24
26
  const envelope = await exportIdentity(name)
25
27
  const passphrase = opts.encrypt
@@ -27,6 +29,10 @@ export default {
27
29
  : undefined
28
30
  await writeIdentityExport(path, await encodeIdentityExport(envelope, passphrase))
29
31
 
32
+ if (isMachine(opts)) {
33
+ output({ name, path, encrypted: Boolean(opts.encrypt) }, opts)
34
+ return
35
+ }
30
36
  log.success(`Exported identity "${name}" → ${path}${opts.encrypt ? ' (encrypted)' : ''}`)
31
37
  if (!opts.encrypt) {
32
38
  log.warn(
@@ -34,7 +40,7 @@ export default {
34
40
  )
35
41
  }
36
42
  } catch (e) {
37
- fatal(e)
43
+ fatal(e, opts)
38
44
  }
39
45
  },
40
46
  } satisfies CommandDefinition