@astrale-os/cli 0.4.0-alpha.13

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 (219) hide show
  1. package/.check-workspace.cjs +40 -0
  2. package/README.md +151 -0
  3. package/dist/astrale.js +59749 -0
  4. package/package.json +90 -0
  5. package/src/command.ts +40 -0
  6. package/src/commands/__tests__/admin-instance.test.ts +73 -0
  7. package/src/commands/__tests__/auth-login.test.ts +178 -0
  8. package/src/commands/__tests__/auth-token.test.ts +223 -0
  9. package/src/commands/__tests__/call.test.ts +72 -0
  10. package/src/commands/__tests__/domain-list.test.ts +74 -0
  11. package/src/commands/__tests__/help-contract.test.ts +136 -0
  12. package/src/commands/__tests__/install-identity-override.test.ts +65 -0
  13. package/src/commands/__tests__/instance-bookmark.test.ts +101 -0
  14. package/src/commands/__tests__/instance-create-hosts.test.ts +29 -0
  15. package/src/commands/__tests__/instance-list-rows.test.ts +63 -0
  16. package/src/commands/__tests__/logs.test.ts +117 -0
  17. package/src/commands/__tests__/ls.test.ts +25 -0
  18. package/src/commands/__tests__/setup-plan.test.ts +61 -0
  19. package/src/commands/admin/status.ts +61 -0
  20. package/src/commands/admin/use.ts +77 -0
  21. package/src/commands/auth/login.ts +82 -0
  22. package/src/commands/auth/logout.ts +50 -0
  23. package/src/commands/auth/status.ts +86 -0
  24. package/src/commands/auth/token.ts +162 -0
  25. package/src/commands/browser.ts +207 -0
  26. package/src/commands/call.ts +300 -0
  27. package/src/commands/describe.ts +182 -0
  28. package/src/commands/domain/install.ts +420 -0
  29. package/src/commands/domain/list.ts +154 -0
  30. package/src/commands/domain/publish.ts +155 -0
  31. package/src/commands/get.ts +60 -0
  32. package/src/commands/identity/create.ts +26 -0
  33. package/src/commands/identity/delete.ts +18 -0
  34. package/src/commands/identity/export.ts +66 -0
  35. package/src/commands/identity/import.ts +101 -0
  36. package/src/commands/identity/list.ts +56 -0
  37. package/src/commands/identity/register.ts +170 -0
  38. package/src/commands/identity/sync.ts +32 -0
  39. package/src/commands/identity/unsync.ts +24 -0
  40. package/src/commands/identity/use.ts +18 -0
  41. package/src/commands/identity/whoami.ts +34 -0
  42. package/src/commands/idp/add.ts +150 -0
  43. package/src/commands/idp/list.ts +57 -0
  44. package/src/commands/idp/refresh.ts +38 -0
  45. package/src/commands/idp/remove.ts +36 -0
  46. package/src/commands/idp/show.ts +29 -0
  47. package/src/commands/instance/active.ts +64 -0
  48. package/src/commands/instance/bookmark.ts +72 -0
  49. package/src/commands/instance/create.ts +69 -0
  50. package/src/commands/instance/delete.ts +72 -0
  51. package/src/commands/instance/forget.ts +26 -0
  52. package/src/commands/instance/list.ts +149 -0
  53. package/src/commands/instance/status.ts +42 -0
  54. package/src/commands/instance/use.ts +210 -0
  55. package/src/commands/logs.ts +347 -0
  56. package/src/commands/ls.ts +229 -0
  57. package/src/commands/query.ts +32 -0
  58. package/src/commands/setup.ts +54 -0
  59. package/src/commands/status.ts +60 -0
  60. package/src/commands/studio.ts +401 -0
  61. package/src/commands/token.ts +77 -0
  62. package/src/commands/update.ts +267 -0
  63. package/src/commands/use.ts +87 -0
  64. package/src/errors.ts +65 -0
  65. package/src/kernel/__tests__/auth.test.ts +77 -0
  66. package/src/kernel/__tests__/errors.test.ts +43 -0
  67. package/src/kernel/__tests__/remote-routing.test.ts +70 -0
  68. package/src/kernel/auth.ts +234 -0
  69. package/src/kernel/ca-fetch.ts +119 -0
  70. package/src/kernel/client.ts +191 -0
  71. package/src/kernel/errors.ts +280 -0
  72. package/src/kernel/expand.ts +217 -0
  73. package/src/kernel/index.ts +14 -0
  74. package/src/kernel/options.ts +22 -0
  75. package/src/kernel/remote-routing.ts +88 -0
  76. package/src/kernel/run.ts +63 -0
  77. package/src/kernel/types.ts +14 -0
  78. package/src/lib/__tests__/admin-target.test.ts +112 -0
  79. package/src/lib/__tests__/binary.test.ts +56 -0
  80. package/src/lib/__tests__/command-dx.test.ts +58 -0
  81. package/src/lib/__tests__/concurrency.test.ts +62 -0
  82. package/src/lib/__tests__/config.test.ts +53 -0
  83. package/src/lib/__tests__/design.test.ts +99 -0
  84. package/src/lib/__tests__/domain-identity.test.ts +60 -0
  85. package/src/lib/__tests__/format.test.ts +22 -0
  86. package/src/lib/__tests__/fs-atomic.test.ts +104 -0
  87. package/src/lib/__tests__/identity.test.ts +79 -0
  88. package/src/lib/__tests__/idp-session.driver.ts +53 -0
  89. package/src/lib/__tests__/idp-session.test.ts +357 -0
  90. package/src/lib/__tests__/idp.test.ts +385 -0
  91. package/src/lib/__tests__/instance-candidates.test.ts +73 -0
  92. package/src/lib/__tests__/instance-target.test.ts +183 -0
  93. package/src/lib/__tests__/instance.test.ts +136 -0
  94. package/src/lib/__tests__/keys.test.ts +129 -0
  95. package/src/lib/__tests__/local-status.test.ts +202 -0
  96. package/src/lib/__tests__/output.test.ts +150 -0
  97. package/src/lib/__tests__/panel.test.ts +40 -0
  98. package/src/lib/__tests__/port.test.ts +44 -0
  99. package/src/lib/__tests__/prompt.test.ts +25 -0
  100. package/src/lib/__tests__/sdk-deps.test.ts +68 -0
  101. package/src/lib/__tests__/self.test.ts +272 -0
  102. package/src/lib/__tests__/studio-server-deps.test.ts +74 -0
  103. package/src/lib/__tests__/table.test.ts +53 -0
  104. package/src/lib/__tests__/update.test.ts +246 -0
  105. package/src/lib/__tests__/use-target.test.ts +56 -0
  106. package/src/lib/__tests__/validation.test.ts +34 -0
  107. package/src/lib/admin-domain.ts +25 -0
  108. package/src/lib/admin-instance.ts +26 -0
  109. package/src/lib/admin-target.ts +217 -0
  110. package/src/lib/binary.ts +131 -0
  111. package/src/lib/browser.ts +150 -0
  112. package/src/lib/command-dx.ts +161 -0
  113. package/src/lib/concurrency.ts +31 -0
  114. package/src/lib/config.ts +45 -0
  115. package/src/lib/domain-identity.ts +49 -0
  116. package/src/lib/env.ts +49 -0
  117. package/src/lib/format.ts +4 -0
  118. package/src/lib/fs-atomic.ts +126 -0
  119. package/src/lib/identity.ts +256 -0
  120. package/src/lib/idp-session.ts +134 -0
  121. package/src/lib/idp.ts +876 -0
  122. package/src/lib/instance-candidates.ts +49 -0
  123. package/src/lib/instance-target.ts +182 -0
  124. package/src/lib/instance.ts +395 -0
  125. package/src/lib/keys.ts +294 -0
  126. package/src/lib/local-status.ts +152 -0
  127. package/src/lib/log.ts +116 -0
  128. package/src/lib/login-flow.ts +164 -0
  129. package/src/lib/meta.ts +86 -0
  130. package/src/lib/output.ts +222 -0
  131. package/src/lib/panel.ts +61 -0
  132. package/src/lib/paths.ts +11 -0
  133. package/src/lib/port.ts +41 -0
  134. package/src/lib/proc.ts +82 -0
  135. package/src/lib/prompt.ts +136 -0
  136. package/src/lib/provision-instance.ts +170 -0
  137. package/src/lib/sdk-deps.ts +104 -0
  138. package/src/lib/self.ts +166 -0
  139. package/src/lib/skills.ts +171 -0
  140. package/src/lib/table.ts +62 -0
  141. package/src/lib/update.ts +315 -0
  142. package/src/lib/use-target.ts +24 -0
  143. package/src/lib/validation.ts +59 -0
  144. package/src/program.ts +200 -0
  145. package/src/registry.ts +59 -0
  146. package/src/setup/__tests__/util.test.ts +29 -0
  147. package/src/setup/engine.ts +83 -0
  148. package/src/setup/render.ts +109 -0
  149. package/src/setup/steps/admin.ts +78 -0
  150. package/src/setup/steps/agent-browser.ts +81 -0
  151. package/src/setup/steps/auth.ts +54 -0
  152. package/src/setup/steps/domain.ts +68 -0
  153. package/src/setup/steps/index.ts +18 -0
  154. package/src/setup/steps/instance.ts +119 -0
  155. package/src/setup/steps/skills-bridge.ts +70 -0
  156. package/src/setup/steps/skills.ts +59 -0
  157. package/src/setup/types.ts +61 -0
  158. package/src/setup/util.ts +34 -0
  159. package/src/test-utils.ts +18 -0
  160. package/studio/client/dist/assets/index-DOwzZAEK.css +1 -0
  161. package/studio/client/dist/assets/index-wtU0Zxhy.js +183 -0
  162. package/studio/client/dist/index.html +13 -0
  163. package/studio/package.json +62 -0
  164. package/studio/server/agent/ask.ts +68 -0
  165. package/studio/server/agent/bridge-mcp.ts +182 -0
  166. package/studio/server/agent/bridge.ts +188 -0
  167. package/studio/server/agent/claude.ts +666 -0
  168. package/studio/server/agent/mock.ts +186 -0
  169. package/studio/server/agent/prompt.ts +202 -0
  170. package/studio/server/agent/registry.ts +29 -0
  171. package/studio/server/agent/runner.ts +484 -0
  172. package/studio/server/agent/schema-map.ts +112 -0
  173. package/studio/server/agent/types.ts +120 -0
  174. package/studio/server/api.ts +574 -0
  175. package/studio/server/cache.ts +138 -0
  176. package/studio/server/detect.ts +81 -0
  177. package/studio/server/domain.ts +70 -0
  178. package/studio/server/index.ts +136 -0
  179. package/studio/server/introspect/anatomy-extras.ts +398 -0
  180. package/studio/server/introspect/anatomy.ts +108 -0
  181. package/studio/server/introspect/bundle.ts +57 -0
  182. package/studio/server/introspect/core-extractor.ts +119 -0
  183. package/studio/server/introspect/core.ts +44 -0
  184. package/studio/server/introspect/diff.ts +133 -0
  185. package/studio/server/introspect/extractor.ts +102 -0
  186. package/studio/server/introspect/hash.ts +21 -0
  187. package/studio/server/introspect/overlay-tsmorph.ts +874 -0
  188. package/studio/server/introspect/overlay.ts +57 -0
  189. package/studio/server/introspect/runtime.ts +99 -0
  190. package/studio/server/introspect/schema-refs.ts +46 -0
  191. package/studio/server/lifecycle.ts +38 -0
  192. package/studio/server/sse.ts +57 -0
  193. package/studio/server/state/baseline.ts +211 -0
  194. package/studio/server/state/catalog.ts +117 -0
  195. package/studio/server/state/comments.ts +321 -0
  196. package/studio/server/state/context.ts +167 -0
  197. package/studio/server/state/copy.ts +156 -0
  198. package/studio/server/state/create.ts +156 -0
  199. package/studio/server/state/documents.ts +70 -0
  200. package/studio/server/state/env.ts +161 -0
  201. package/studio/server/state/git.ts +75 -0
  202. package/studio/server/state/handoff.ts +55 -0
  203. package/studio/server/state/harness-gateway.ts +181 -0
  204. package/studio/server/state/harness-token.ts +0 -0
  205. package/studio/server/state/instance.ts +244 -0
  206. package/studio/server/state/integrations.ts +55 -0
  207. package/studio/server/state/layout.ts +55 -0
  208. package/studio/server/state/settings.ts +39 -0
  209. package/studio/server/state/store.ts +97 -0
  210. package/studio/server/state/updates.ts +63 -0
  211. package/studio/server/state/usage.ts +37 -0
  212. package/studio/server/state/views.ts +138 -0
  213. package/studio/server/state/visibility.ts +33 -0
  214. package/studio/server/watch.ts +81 -0
  215. package/studio/server/workspace-state.ts +26 -0
  216. package/studio/server/workspace-watch.ts +101 -0
  217. package/studio/shared/types.ts +873 -0
  218. package/studio/tsconfig.json +23 -0
  219. package/tsconfig.json +14 -0
@@ -0,0 +1,54 @@
1
+ import type { SetupContext, SetupStep } from '../types'
2
+
3
+ import { readLocalStatus } from '../../lib/local-status'
4
+ import { log } from '../../lib/log'
5
+ import { loginViaIdp } from '../../lib/login-flow'
6
+
7
+ const FIX = 'astrale auth login'
8
+
9
+ /**
10
+ * Step 1 — a WorkOS-backed identity with a cached session. Provisioning an
11
+ * instance requires an `idp` identity (not the local seed key), and stale
12
+ * access tokens refresh on the first command when a refresh token is cached.
13
+ */
14
+ export const authStep: SetupStep = {
15
+ id: 'auth',
16
+ title: 'Sign in',
17
+ group: 'connect',
18
+
19
+ async detect() {
20
+ const { identity } = await readLocalStatus()
21
+ if (!identity || identity.source !== 'idp') {
22
+ return { state: 'gap', summary: 'Not signed in', fixHint: FIX }
23
+ }
24
+ const session = identity.session
25
+ if (!session || !session.cached) {
26
+ return { state: 'gap', summary: `No cached token for "${identity.name}"`, fixHint: FIX }
27
+ }
28
+ if (session.requiresLogin) {
29
+ return {
30
+ state: 'gap',
31
+ summary: `Session for "${identity.name}" requires login`,
32
+ fixHint: FIX,
33
+ }
34
+ }
35
+ return {
36
+ state: 'satisfied',
37
+ summary: `Signed in as ${identity.name}`,
38
+ detail: identity.subject,
39
+ }
40
+ },
41
+
42
+ async ensure(ctx: SetupContext) {
43
+ const detection = await authStep.detect(ctx)
44
+ if (detection.state === 'satisfied') {
45
+ log.success(detection.summary)
46
+ return 'unchanged'
47
+ }
48
+
49
+ log.step('Sign in with your Astrale account (a URL + code will appear) …')
50
+ const { identityName } = await loginViaIdp({ use: true })
51
+ log.success(`Signed in as "${identityName}"`)
52
+ return 'fixed'
53
+ },
54
+ }
@@ -0,0 +1,68 @@
1
+ import { existsSync } from 'node:fs'
2
+ import { join } from 'node:path'
3
+
4
+ import type { SetupStep } from '../types'
5
+
6
+ import { readLocalStatus } from '../../lib/local-status'
7
+ import { log } from '../../lib/log'
8
+ import { runInherit } from '../../lib/proc'
9
+ import { confirmDefaultYes, promptText } from '../../lib/prompt'
10
+
11
+ /** A scaffolded domain project carries this config at its root. */
12
+ function hasDomainProject(): boolean {
13
+ return existsSync(join(process.cwd(), 'astrale.config.ts'))
14
+ }
15
+
16
+ const FIX = 'npx create-astrale-domain <name> --instance <slug>'
17
+
18
+ /**
19
+ * Equip — scaffold a first domain in the current directory via
20
+ * `create-astrale-domain`, pre-wired to the active instance. The scaffold also
21
+ * ships the astrale-domain authoring skill, so this covers that skill too.
22
+ */
23
+ export const domainStep: SetupStep = {
24
+ id: 'domain',
25
+ title: 'Scaffold a domain',
26
+ group: 'equip',
27
+
28
+ async detect() {
29
+ if (hasDomainProject()) {
30
+ return { state: 'satisfied', summary: 'domain project detected in this directory' }
31
+ }
32
+ return { state: 'gap', summary: 'no domain project here', fixHint: FIX }
33
+ },
34
+
35
+ async ensure() {
36
+ if (hasDomainProject()) {
37
+ log.success('Domain project already in this directory')
38
+ return 'unchanged'
39
+ }
40
+
41
+ if (!(await confirmDefaultYes('Scaffold a new domain project here?'))) {
42
+ log.dim(` Skipped — scaffold later: ${FIX}`)
43
+ return 'skipped'
44
+ }
45
+
46
+ const name = await promptText('Domain project name', {
47
+ default: 'my-domain',
48
+ validate: (v) =>
49
+ /^[a-z0-9][a-z0-9-]*$/.test(v) ? true : 'Use a lowercase name like my-domain',
50
+ })
51
+ if (!name) {
52
+ log.dim(' Skipped — no name given.')
53
+ return 'skipped'
54
+ }
55
+
56
+ const slug = (await readLocalStatus()).instance?.active
57
+ const args = ['create-astrale-domain', name, ...(slug ? ['--instance', slug] : [])]
58
+ log.step(`npx ${args.join(' ')}`)
59
+ if ((await runInherit('npx', args)) !== 0) {
60
+ log.warn('Scaffold failed — see the output above.')
61
+ return 'failed'
62
+ }
63
+
64
+ log.success(`Domain scaffolded → ./${name}`)
65
+ log.dim(` Next: cd ${name} && pnpm install && pnpm prod`)
66
+ return 'fixed'
67
+ },
68
+ }
@@ -0,0 +1,18 @@
1
+ import type { SetupStep } from '../types'
2
+
3
+ import { adminStep } from './admin'
4
+ import { agentBrowserStep } from './agent-browser'
5
+ import { authStep } from './auth'
6
+ import { domainStep } from './domain'
7
+ import { instanceStep } from './instance'
8
+ import { skillsStep } from './skills'
9
+ import { skillsBridgeStep } from './skills-bridge'
10
+
11
+ /** Required prerequisites, walked in order — each is a precondition for the next. */
12
+ export const CONNECT_STEPS: SetupStep[] = [authStep, adminStep, instanceStep]
13
+
14
+ /** Optional extras, offered together as a pre-checked multi-select. The skills
15
+ * bridge runs first so the per-skill steps below detect against the bridged state. */
16
+ export const EQUIP_STEPS: SetupStep[] = [skillsBridgeStep, skillsStep, agentBrowserStep, domainStep]
17
+
18
+ export const ALL_STEPS: SetupStep[] = [...CONNECT_STEPS, ...EQUIP_STEPS]
@@ -0,0 +1,119 @@
1
+ import chalk from 'chalk'
2
+
3
+ import type { SetupContext, SetupStep } from '../types'
4
+
5
+ import { withAdminKernelClient } from '../../kernel/client'
6
+ import { ADMIN_INSTANCE, type InstanceInfo } from '../../lib/admin-instance'
7
+ import { normalizeInstanceKernelUrl, setActive, upsertManagedBookmark } from '../../lib/instance'
8
+ import { readLocalStatus } from '../../lib/local-status'
9
+ import { log, withSpinner } from '../../lib/log'
10
+ import { renderInstanceHero } from '../../lib/panel'
11
+ import { confirmDefaultYes, promptText, selectFrom } from '../../lib/prompt'
12
+ import { provisionInstance } from '../../lib/provision-instance'
13
+ import { guiOrigin, slugError } from '../util'
14
+
15
+ /** All admin-managed instances; degrades to [] when the admin kernel is unreachable. */
16
+ async function fetchManaged(ctx: SetupContext): Promise<InstanceInfo[]> {
17
+ try {
18
+ return await withSpinner('Checking for existing instances', !ctx.machine, () =>
19
+ withAdminKernelClient(
20
+ ctx.opts,
21
+ async (client) =>
22
+ (await client.client.call(`${ADMIN_INSTANCE}/list`, {})) as InstanceInfo[],
23
+ ),
24
+ )
25
+ } catch {
26
+ return []
27
+ }
28
+ }
29
+
30
+ /** Print the click-inviting hero for a freshly-active instance. */
31
+ function hero(slug: string, kernelUrl: string): void {
32
+ console.log('')
33
+ console.log(renderInstanceHero(slug, guiOrigin(kernelUrl)))
34
+ console.log('')
35
+ }
36
+
37
+ async function adopt(info: InstanceInfo): Promise<void> {
38
+ const { repointedFrom } = await upsertManagedBookmark(info.slug, info.slug, info.url)
39
+ await setActive(info.slug)
40
+ if (repointedFrom) {
41
+ log.warn(
42
+ `Bookmark "${info.slug}" repointed: ${repointedFrom} → ${normalizeInstanceKernelUrl(info.url)}`,
43
+ )
44
+ }
45
+ log.success(`Active instance: ${info.slug}`)
46
+ hero(info.slug, info.url)
47
+ }
48
+
49
+ /**
50
+ * Step 3 — an active instance, the heart of the flow. If the user already has
51
+ * managed instances, offer to adopt one or create another; otherwise offer to
52
+ * provision their first. Either way ends on the instance hero.
53
+ */
54
+ export const instanceStep: SetupStep = {
55
+ id: 'instance',
56
+ title: 'Active instance',
57
+ group: 'connect',
58
+
59
+ async detect(ctx: SetupContext) {
60
+ const { instance } = await readLocalStatus()
61
+ if (instance) {
62
+ return { state: 'satisfied', summary: `${instance.active} (${guiOrigin(instance.url)})` }
63
+ }
64
+ return {
65
+ state: 'gap',
66
+ summary: 'No active instance',
67
+ fixHint: ctx.slug ? `astrale instance create ${ctx.slug}` : 'astrale instance create <slug>',
68
+ }
69
+ },
70
+
71
+ async ensure(ctx: SetupContext) {
72
+ const { instance } = await readLocalStatus()
73
+ if (instance) {
74
+ log.success(`Active instance: ${instance.active} (${guiOrigin(instance.url)})`)
75
+ return 'unchanged'
76
+ }
77
+
78
+ const managed = await fetchManaged(ctx)
79
+
80
+ // Pick an existing instance or fall through to creating a new one.
81
+ if (managed.length > 0) {
82
+ const choice = await selectFrom<InstanceInfo | 'create'>(
83
+ 'No active instance. Pick one or create a new instance:',
84
+ [
85
+ ...managed.map((info) => ({
86
+ label: `${info.slug} ${chalk.dim(guiOrigin(info.url))}`,
87
+ value: info as InstanceInfo | 'create',
88
+ })),
89
+ { label: chalk.cyan('➕ Create a new instance'), value: 'create' as const },
90
+ ],
91
+ )
92
+ if (choice === null) {
93
+ log.dim(' Skipped — no active instance set.')
94
+ return 'skipped'
95
+ }
96
+ if (choice !== 'create') {
97
+ await adopt(choice)
98
+ return 'fixed'
99
+ }
100
+ } else {
101
+ const yes = await confirmDefaultYes('No instances yet. Provision your first one now?')
102
+ if (!yes) {
103
+ log.dim(' Skipped — create one later: astrale instance create <slug>')
104
+ return 'skipped'
105
+ }
106
+ }
107
+
108
+ const slug =
109
+ ctx.slug ?? (await promptText('Pick a slug for your instance', { validate: slugError }))
110
+ if (!slug) {
111
+ log.dim(' Skipped — no slug given.')
112
+ return 'skipped'
113
+ }
114
+
115
+ const { created } = await provisionInstance(slug, ctx.opts)
116
+ hero(slug, created.url)
117
+ return 'fixed'
118
+ },
119
+ }
@@ -0,0 +1,70 @@
1
+ import { existsSync, readdirSync } from 'node:fs'
2
+ import { join } from 'node:path'
3
+
4
+ import type { SetupStep } from '../types'
5
+
6
+ import { log } from '../../lib/log'
7
+ import { ensureSkillsBridge, skillsBridgeStatus } from '../../lib/skills'
8
+
9
+ /**
10
+ * Equip — make a workspace's own staged skills loadable. Agent tooling stages
11
+ * skills under `.agents/skills`, but the harness (Claude Code et al.) only loads
12
+ * `.claude/skills` (walking up to the project root). A single
13
+ * `.claude/skills -> ../.agents/skills` symlink bridges every staged skill —
14
+ * present and future — for this workspace and every project nested under it.
15
+ * This is exactly how this monorepo wires its own skills; doing it in `setup`
16
+ * means a skill landing in `.agents/skills` is loaded, not silently invisible
17
+ * (the gap that let agent-browser report "installed" yet never load).
18
+ *
19
+ * Runs first in the equip group so the per-skill steps below detect against the
20
+ * already-bridged state. Idempotent and non-destructive.
21
+ */
22
+ function countStaged(root: string): number {
23
+ try {
24
+ const dir = join(root, '.agents', 'skills')
25
+ return readdirSync(dir).filter((e) => existsSync(join(dir, e, 'SKILL.md'))).length
26
+ } catch {
27
+ return 0
28
+ }
29
+ }
30
+
31
+ export const skillsBridgeStep: SetupStep = {
32
+ id: 'skills-bridge',
33
+ title: 'workspace skills bridge',
34
+ group: 'equip',
35
+
36
+ async detect() {
37
+ const s = skillsBridgeStatus()
38
+ if (s.kind === 'none') return { state: 'satisfied', summary: 'no workspace skills to bridge' }
39
+ if (s.kind === 'bridged')
40
+ return { state: 'satisfied', summary: '.agents/skills bridged → the harness loads them' }
41
+ if (s.kind === 'foreign')
42
+ return { state: 'satisfied', summary: '.claude/skills already present — left as-is' }
43
+ const n = countStaged(s.root)
44
+ return {
45
+ state: 'gap',
46
+ summary: `${n} skill${n === 1 ? '' : 's'} in .agents/skills not loaded by the harness`,
47
+ fixHint: `ln -s ../.agents/skills ${s.link}`,
48
+ }
49
+ },
50
+
51
+ async ensure() {
52
+ const before = skillsBridgeStatus()
53
+ if (before.kind === 'none' || before.kind === 'bridged') return 'unchanged'
54
+ if (before.kind === 'foreign') {
55
+ log.dim(' .claude/skills already exists — leaving it untouched.')
56
+ return 'unchanged'
57
+ }
58
+ const after = ensureSkillsBridge()
59
+ if (after.kind === 'bridged') {
60
+ log.success(
61
+ `Workspace skills bridged — ${join(after.root, '.agents/skills')} → .claude/skills`,
62
+ )
63
+ return 'fixed'
64
+ }
65
+ log.warn(
66
+ 'Could not create the skills bridge — create it manually: ln -s ../.agents/skills .claude/skills',
67
+ )
68
+ return 'failed'
69
+ },
70
+ }
@@ -0,0 +1,59 @@
1
+ import type { SetupStep } from '../types'
2
+
3
+ import { log } from '../../lib/log'
4
+ import {
5
+ ASTRALE_CLI_SKILL,
6
+ ASTRALE_DOMAIN_SKILL,
7
+ detectSkill,
8
+ installSkills,
9
+ SKILL_INSTALL_HINT,
10
+ } from '../../lib/skills'
11
+
12
+ const FIX = SKILL_INSTALL_HINT
13
+
14
+ /**
15
+ * Equip — the astrale agent skills. Both the astrale-cli (ops) and astrale-domain
16
+ * (authoring) skills publish from the one public `astrale-os/cli` repo, so a
17
+ * single `npx skills add astrale-os/cli -g` installs both, globally. We don't
18
+ * reimplement skill installation; we detect presence and delegate to `npx skills
19
+ * add` (the skill package manager). A global (user-level) install equips every
20
+ * project on the machine, so a freshly scaffolded domain has the skills without
21
+ * shipping its own copy.
22
+ */
23
+ const bothInstalled = (): boolean =>
24
+ detectSkill(ASTRALE_CLI_SKILL).installed && detectSkill(ASTRALE_DOMAIN_SKILL).installed
25
+
26
+ export const skillsStep: SetupStep = {
27
+ id: 'skills',
28
+ title: 'astrale agent skills (cli + domain)',
29
+ group: 'equip',
30
+
31
+ async detect() {
32
+ if (bothInstalled()) {
33
+ return { state: 'satisfied', summary: 'astrale-cli + astrale-domain skills installed' }
34
+ }
35
+ const have = detectSkill(ASTRALE_CLI_SKILL).installed
36
+ ? 'astrale-domain'
37
+ : 'astrale agent skills'
38
+ return { state: 'gap', summary: `${have} not installed`, fixHint: FIX }
39
+ },
40
+
41
+ async ensure() {
42
+ if (bothInstalled()) {
43
+ log.success('astrale-cli + astrale-domain skills already installed')
44
+ return 'unchanged'
45
+ }
46
+
47
+ log.step(`Installing the astrale agent skills — ${FIX}`)
48
+ if (!(await installSkills())) {
49
+ log.warn(`Skill install did not complete — run it later: ${FIX}`)
50
+ return 'failed'
51
+ }
52
+ if (bothInstalled()) {
53
+ log.success('astrale-cli + astrale-domain skills installed')
54
+ } else {
55
+ log.success('astrale-cli skill installed')
56
+ }
57
+ return 'fixed'
58
+ },
59
+ }
@@ -0,0 +1,61 @@
1
+ import type { AdminTargetCommandOpts } from '../lib/admin-target'
2
+
3
+ /**
4
+ * The setup reconciler models onboarding as a list of small, independent steps,
5
+ * each able to (a) `detect` its state without side effects and (b) `ensure` its
6
+ * state interactively, taking the user by the hand. `detect` powers the
7
+ * read-only plan + checklist (and the agent-facing `--plan` JSON); `ensure` is
8
+ * the hand-held fix. Both are idempotent — re-running setup when everything is
9
+ * already satisfied just prints a row of ✔.
10
+ */
11
+
12
+ export type StepState = 'satisfied' | 'gap' | 'broken'
13
+
14
+ export type StepDetection = {
15
+ state: StepState
16
+ /** One-line status for the checklist (no symbol — the renderer adds it). */
17
+ summary: string
18
+ /** Optional dim detail line. */
19
+ detail?: string
20
+ /** The granular command an agent (or a human) would run to close the gap. */
21
+ fixHint?: string
22
+ }
23
+
24
+ export type StepOutcome = 'fixed' | 'unchanged' | 'skipped' | 'failed'
25
+
26
+ export type SetupOpts = AdminTargetCommandOpts & {
27
+ /** Read-only: print the plan and exit without mutating anything. */
28
+ plan?: boolean
29
+ json?: boolean
30
+ raw?: boolean
31
+ // Global flags (program.ts) that force non-interactive.
32
+ ci?: boolean
33
+ noPrompt?: boolean
34
+ // Threaded into admin-kernel calls (provisioning, instance listing).
35
+ as?: string
36
+ creds?: string
37
+ timeout?: string
38
+ debug?: boolean
39
+ }
40
+
41
+ export type SetupContext = {
42
+ /** TTY and not --ci / --no-prompt / CI: ok to prompt and mutate. */
43
+ interactive: boolean
44
+ /** isMachine(opts): --json / --raw or piped — structured output, no spinners. */
45
+ machine: boolean
46
+ opts: SetupOpts
47
+ /** Instance slug hint from the positional arg, used when provisioning. */
48
+ slug?: string
49
+ }
50
+
51
+ export type StepGroup = 'connect' | 'equip'
52
+
53
+ export type SetupStep = {
54
+ id: string
55
+ title: string
56
+ group: StepGroup
57
+ /** Read-only probe for the plan + checklist. Never prompts, never mutates. */
58
+ detect: (ctx: SetupContext) => Promise<StepDetection>
59
+ /** Interactive, idempotent hand-holding. May print rich output (e.g. a hero). */
60
+ ensure: (ctx: SetupContext) => Promise<StepOutcome>
61
+ }
@@ -0,0 +1,34 @@
1
+ import { validateSlug, validateUrl } from '../lib/validation'
2
+
3
+ /**
4
+ * The clickable GUI origin for an instance — its kernel URL stripped of the
5
+ * `/api` path bookmarks carry (`https://app.eu.astrale.ai/api` →
6
+ * `https://app.eu.astrale.ai`). This is the URL we put in front of the user.
7
+ */
8
+ export function guiOrigin(kernelUrl: string): string {
9
+ try {
10
+ return new URL(kernelUrl).origin
11
+ } catch {
12
+ return kernelUrl
13
+ }
14
+ }
15
+
16
+ /** inquirer `validate`: true when the slug is valid, else the human message. */
17
+ export function slugError(value: string): true | string {
18
+ try {
19
+ validateSlug(value)
20
+ return true
21
+ } catch (e) {
22
+ return e instanceof Error ? e.message : 'Invalid slug'
23
+ }
24
+ }
25
+
26
+ /** inquirer `validate`: true when the value is an http(s) URL, else the message. */
27
+ export function urlError(value: string): true | string {
28
+ try {
29
+ validateUrl(value)
30
+ return true
31
+ } catch (e) {
32
+ return e instanceof Error ? e.message : 'Invalid URL'
33
+ }
34
+ }
@@ -0,0 +1,18 @@
1
+ import { mkdtemp, mkdir } from 'node:fs/promises'
2
+ import { tmpdir } from 'node:os'
3
+ import { join } from 'node:path'
4
+
5
+ import { createPaths, type Paths } from './lib/env'
6
+
7
+ export type TestEnv = {
8
+ paths: Paths
9
+ tmp: string
10
+ }
11
+
12
+ export async function createTestEnv(): Promise<TestEnv> {
13
+ const tmp = await mkdtemp(join(tmpdir(), 'astrale-test-'))
14
+ const paths = createPaths(tmp)
15
+ await mkdir(paths.keys, { recursive: true })
16
+ await mkdir(paths.data, { recursive: true })
17
+ return { paths, tmp }
18
+ }