@datumos/datum-cli 0.0.0 → 0.589.0
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/LICENSE +201 -0
- package/NOTICE +8 -0
- package/README.md +37 -0
- package/cli/a2aCommand.js +225 -0
- package/cli/artifactsPublish.js +356 -0
- package/cli/authCommands.js +168 -0
- package/cli/bundleSignature.js +111 -0
- package/cli/commandTree.js +243 -0
- package/cli/credentialProcess.js +54 -0
- package/cli/credentials.js +93 -0
- package/cli/datum.js +814 -0
- package/cli/entraLogin.js +254 -0
- package/cli/help.js +509 -0
- package/cli/invoke.js +427 -0
- package/cli/mcpServe.js +361 -0
- package/cli/outputsDownload.js +126 -0
- package/cli/previewCommand.js +333 -0
- package/cli/satellitePairToken.js +48 -0
- package/cli/secureOrigin.js +37 -0
- package/cli/selfUpdate.js +779 -0
- package/cli/spaceManifestCommand.js +495 -0
- package/cli/spaceManifestDir.js +481 -0
- package/cli/spaceRepoTemplate.js +463 -0
- package/cli/updateCommand.js +184 -0
- package/npm-shrinkwrap.json +1231 -0
- package/openapi/spec.js +51199 -0
- package/package.json +25 -3
- package/spaces/reservedHostLabels.js +66 -0
|
@@ -0,0 +1,463 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The files `datum space-manifest init <dir>` writes (#1809): a repository
|
|
3
|
+
* that keeps ONE space's configuration as code — the manifest files under
|
|
4
|
+
* `space/`, a CI that plans on a pull request and applies on `main`, a
|
|
5
|
+
* daily sync that brings panel edits back as a pull request, and the rules
|
|
6
|
+
* for whoever (or whatever agent) edits it. No Terraform, no state: the
|
|
7
|
+
* server plans every change against the live space
|
|
8
|
+
* (docs/space-admin/space-repository.md).
|
|
9
|
+
*/
|
|
10
|
+
/** Where the manifest files live inside the repository. */
|
|
11
|
+
export const SPACE_REPO_MANIFEST_DIR = "space";
|
|
12
|
+
/** The repository secret the workflows read — a key bound to the space. */
|
|
13
|
+
export const SPACE_REPO_KEY_SECRET = "DATUM_SPACE_API_KEY";
|
|
14
|
+
/** The repository variable with the organisation host. */
|
|
15
|
+
export const SPACE_REPO_BASE_URL_VAR = "DATUM_BASE_URL";
|
|
16
|
+
/** The branch the sync workflow opens its pull request from. */
|
|
17
|
+
export const SPACE_REPO_SYNC_BRANCH = "datum/sync-from-space";
|
|
18
|
+
/** The public package the workflows install (docs/cli.md "Running it"). */
|
|
19
|
+
export const DATUM_CLI_NPM_PACKAGE = "@datumos/datum-cli";
|
|
20
|
+
/** The recommended repository name: the team's namespace first, like the
|
|
21
|
+
* namespace's other repositories (repositories are created in Cloud Market,
|
|
22
|
+
* in a namespace's "GitHub repositories" purchase, never directly in GitHub). */
|
|
23
|
+
export function recommendedSpaceRepoName(space, namespace = "<namespace>") {
|
|
24
|
+
return `${namespace}-datum-space-${space}`;
|
|
25
|
+
}
|
|
26
|
+
const DIR = SPACE_REPO_MANIFEST_DIR;
|
|
27
|
+
function installCli() {
|
|
28
|
+
return ` - uses: actions/setup-node@v4
|
|
29
|
+
with: { node-version: 22 }
|
|
30
|
+
- name: Install the datum CLI
|
|
31
|
+
run: |
|
|
32
|
+
npm install -g ${DATUM_CLI_NPM_PACKAGE}`;
|
|
33
|
+
}
|
|
34
|
+
function planApplyWorkflow(space) {
|
|
35
|
+
return `# The space's configuration as code: plan on a pull request, apply on main.
|
|
36
|
+
# Written by \`datum space-manifest init\` — docs/space-admin/space-repository.md.
|
|
37
|
+
#
|
|
38
|
+
# Repository settings it needs:
|
|
39
|
+
# secret ${SPACE_REPO_KEY_SECRET} a key bound to the space "${space}", role admin
|
|
40
|
+
# (panel: Settings → API keys)
|
|
41
|
+
# variable ${SPACE_REPO_BASE_URL_VAR} the organisation host, e.g. https://datum.example.com
|
|
42
|
+
name: Datum space
|
|
43
|
+
|
|
44
|
+
on:
|
|
45
|
+
# The plan needs the space key, so it runs as pull_request_TARGET: the
|
|
46
|
+
# workflow comes from the base branch (a pull request cannot edit what
|
|
47
|
+
# runs with the key) and the pull request is checked out only as DATA —
|
|
48
|
+
# the manifest files — never executed. Every pull request gets a plan
|
|
49
|
+
# (no paths filter), so a required "plan" check never blocks one that
|
|
50
|
+
# does not touch ${DIR}/.
|
|
51
|
+
pull_request_target:
|
|
52
|
+
push:
|
|
53
|
+
branches: [main]
|
|
54
|
+
paths: ["${DIR}/**", ".github/workflows/datum-space.yml"]
|
|
55
|
+
# Run on main to re-apply the repository — e.g. after closing a sync pull
|
|
56
|
+
# request to revert an edit made in the panel. Tick "prune" to delete the
|
|
57
|
+
# entries whose files a merged pull request removed.
|
|
58
|
+
workflow_dispatch:
|
|
59
|
+
inputs:
|
|
60
|
+
prune:
|
|
61
|
+
description: Delete space entries the files no longer name (within the kinds space/ declares)
|
|
62
|
+
type: boolean
|
|
63
|
+
default: false
|
|
64
|
+
|
|
65
|
+
permissions:
|
|
66
|
+
contents: read
|
|
67
|
+
|
|
68
|
+
env:
|
|
69
|
+
DATUM_BASE_URL: \${{ vars.${SPACE_REPO_BASE_URL_VAR} }}
|
|
70
|
+
DATUM_SPACE: ${space}
|
|
71
|
+
|
|
72
|
+
jobs:
|
|
73
|
+
plan:
|
|
74
|
+
if: github.event_name == 'pull_request_target'
|
|
75
|
+
runs-on: [self-hosted, ubuntu-latest, ARM64]
|
|
76
|
+
# Per pull request: a new push supersedes that PR's running plan only.
|
|
77
|
+
concurrency:
|
|
78
|
+
group: datum-space-plan-\${{ github.event.pull_request.number }}
|
|
79
|
+
cancel-in-progress: true
|
|
80
|
+
permissions:
|
|
81
|
+
contents: read
|
|
82
|
+
pull-requests: write
|
|
83
|
+
steps:
|
|
84
|
+
# The pull request's files land in pr/ as data: no script, hook, .npmrc
|
|
85
|
+
# or workflow of theirs runs — the steps below run from the workspace
|
|
86
|
+
# root with the base branch's definitions.
|
|
87
|
+
- uses: actions/checkout@v6
|
|
88
|
+
with:
|
|
89
|
+
ref: \${{ github.event.pull_request.head.sha }}
|
|
90
|
+
path: pr
|
|
91
|
+
persist-credentials: false
|
|
92
|
+
${installCli()}
|
|
93
|
+
- name: Plan (dry run against the live space)
|
|
94
|
+
run: |
|
|
95
|
+
if [ ! -d pr/${DIR} ]; then echo "No ${DIR}/ yet — run the Datum space sync workflow to import the space."; exit 0; fi
|
|
96
|
+
# A refused plan replaces the comment too — never leave an older,
|
|
97
|
+
# successful plan standing on a pull request that no longer plans.
|
|
98
|
+
if ! datum space-manifest push pr/${DIR} --dry-run --markdown plan.md 2> plan.err; then
|
|
99
|
+
cat plan.err
|
|
100
|
+
{ printf '## Datum space plan\\n\\n**The plan was refused** — nothing can be applied until this is fixed:\\n\\n\`\`\`\\n'; cat plan.err; printf '\`\`\`\\n'; } > plan.md
|
|
101
|
+
exit 1
|
|
102
|
+
fi
|
|
103
|
+
env:
|
|
104
|
+
DATUM_API_KEY: \${{ secrets.${SPACE_REPO_KEY_SECRET} }}
|
|
105
|
+
- name: Comment the plan on the pull request
|
|
106
|
+
if: always() && hashFiles('plan.md') != ''
|
|
107
|
+
uses: actions/github-script@v7
|
|
108
|
+
with:
|
|
109
|
+
script: |
|
|
110
|
+
const fs = require('fs');
|
|
111
|
+
const marker = '<!-- datum-space-plan -->';
|
|
112
|
+
const body = \`\${marker}\\n\${fs.readFileSync('plan.md', 'utf8')}\`;
|
|
113
|
+
const { owner, repo } = context.repo;
|
|
114
|
+
const issue_number = context.payload.pull_request.number;
|
|
115
|
+
const comments = await github.paginate(github.rest.issues.listComments, { owner, repo, issue_number });
|
|
116
|
+
const mine = comments.find((c) => c.body?.startsWith(marker));
|
|
117
|
+
if (mine) await github.rest.issues.updateComment({ owner, repo, comment_id: mine.id, body });
|
|
118
|
+
else await github.rest.issues.createComment({ owner, repo, issue_number, body });
|
|
119
|
+
|
|
120
|
+
apply:
|
|
121
|
+
# push / manual run on main ONLY — a pull_request_target run carries
|
|
122
|
+
# ref = main too, and must never re-apply main because a PR was opened.
|
|
123
|
+
if: (github.event_name == 'push' || github.event_name == 'workflow_dispatch') && github.ref == 'refs/heads/main'
|
|
124
|
+
runs-on: [self-hosted, ubuntu-latest, ARM64]
|
|
125
|
+
# Applies queue one after another; a newer pending apply replaces an
|
|
126
|
+
# older pending one, which is fine — both apply main as it is now.
|
|
127
|
+
concurrency:
|
|
128
|
+
group: datum-space-apply
|
|
129
|
+
cancel-in-progress: false
|
|
130
|
+
steps:
|
|
131
|
+
- uses: actions/checkout@v6
|
|
132
|
+
${installCli()}
|
|
133
|
+
- name: Apply
|
|
134
|
+
run: |
|
|
135
|
+
if [ ! -d ${DIR} ]; then echo "No ${DIR}/ yet — nothing to apply."; exit 0; fi
|
|
136
|
+
datum space-manifest push ${DIR} $PRUNE --markdown apply.md
|
|
137
|
+
cat apply.md >> "$GITHUB_STEP_SUMMARY"
|
|
138
|
+
env:
|
|
139
|
+
DATUM_API_KEY: \${{ secrets.${SPACE_REPO_KEY_SECRET} }}
|
|
140
|
+
PRUNE: \${{ inputs.prune && '--prune' || '' }}
|
|
141
|
+
# After a clean apply the space equals the files, so a second dry run
|
|
142
|
+
# must plan zero writes. A write here = a value the server normalises
|
|
143
|
+
# (write it the way the plan shows) or a panel edit racing the apply.
|
|
144
|
+
- name: Verify convergence
|
|
145
|
+
run: |
|
|
146
|
+
if [ ! -d ${DIR} ]; then exit 0; fi
|
|
147
|
+
datum space-manifest push ${DIR} $PRUNE --dry-run --expect-empty
|
|
148
|
+
env:
|
|
149
|
+
DATUM_API_KEY: \${{ secrets.${SPACE_REPO_KEY_SECRET} }}
|
|
150
|
+
PRUNE: \${{ inputs.prune && '--prune' || '' }}
|
|
151
|
+
`;
|
|
152
|
+
}
|
|
153
|
+
function syncWorkflow(space) {
|
|
154
|
+
return `# Brings the space's live configuration into the repository as a pull request.
|
|
155
|
+
# Written by \`datum space-manifest init\` — docs/space-admin/space-repository.md.
|
|
156
|
+
#
|
|
157
|
+
# - First run = the IMPORT (Actions → Datum space sync → Run workflow): a new
|
|
158
|
+
# repository has no ${DIR}/ yet; merge the pull request it opens.
|
|
159
|
+
# - Every weekday morning after that = DRIFT: an edit made in the panel comes
|
|
160
|
+
# back as a pull request. Merge it to keep the edit; close it and the next
|
|
161
|
+
# apply restores what the repository says.
|
|
162
|
+
#
|
|
163
|
+
# The pull request is opened with the workflow's GITHUB_TOKEN, for which
|
|
164
|
+
# GitHub starts no workflows: its plan does not run. A sync pull request
|
|
165
|
+
# mirrors the space, so its plan is empty by construction.
|
|
166
|
+
name: Datum space sync
|
|
167
|
+
|
|
168
|
+
on:
|
|
169
|
+
workflow_dispatch:
|
|
170
|
+
schedule:
|
|
171
|
+
- cron: "0 5 * * 1-5"
|
|
172
|
+
|
|
173
|
+
# Its own group: a queued sync must never cancel a pending apply (GitHub
|
|
174
|
+
# keeps one pending run per group). Instead the sync stays away from an
|
|
175
|
+
# apply in flight — it would read a half-applied space — see the steps.
|
|
176
|
+
concurrency:
|
|
177
|
+
group: datum-space-sync
|
|
178
|
+
cancel-in-progress: false
|
|
179
|
+
|
|
180
|
+
permissions:
|
|
181
|
+
actions: read
|
|
182
|
+
contents: write
|
|
183
|
+
pull-requests: write
|
|
184
|
+
|
|
185
|
+
env:
|
|
186
|
+
DATUM_BASE_URL: \${{ vars.${SPACE_REPO_BASE_URL_VAR} }}
|
|
187
|
+
DATUM_SPACE: ${space}
|
|
188
|
+
|
|
189
|
+
jobs:
|
|
190
|
+
sync:
|
|
191
|
+
runs-on: [self-hosted, ubuntu-latest, ARM64]
|
|
192
|
+
steps:
|
|
193
|
+
- uses: actions/checkout@v6
|
|
194
|
+
${installCli()}
|
|
195
|
+
- name: Wait until no apply is running
|
|
196
|
+
id: idle
|
|
197
|
+
env:
|
|
198
|
+
GH_TOKEN: \${{ github.token }}
|
|
199
|
+
run: |
|
|
200
|
+
applying() {
|
|
201
|
+
gh run list --workflow datum-space.yml --limit 20 --json status,event \\
|
|
202
|
+
--jq '[.[] | select(.event != "pull_request_target" and .status != "completed")] | length'
|
|
203
|
+
}
|
|
204
|
+
for _ in $(seq 1 30); do [ "$(applying)" = "0" ] && break; sleep 20; done
|
|
205
|
+
if [ "$(applying)" != "0" ]; then
|
|
206
|
+
echo "An apply is still running — skipping this sync; the next one sees the result."
|
|
207
|
+
echo "skip=1" >> "$GITHUB_OUTPUT"
|
|
208
|
+
fi
|
|
209
|
+
- name: Pull the live configuration
|
|
210
|
+
if: steps.idle.outputs.skip != '1'
|
|
211
|
+
run: datum space-manifest pull ${DIR}
|
|
212
|
+
env:
|
|
213
|
+
DATUM_API_KEY: \${{ secrets.${SPACE_REPO_KEY_SECRET} }}
|
|
214
|
+
- name: Open or update the sync pull request
|
|
215
|
+
if: steps.idle.outputs.skip != '1'
|
|
216
|
+
env:
|
|
217
|
+
GH_TOKEN: \${{ github.token }}
|
|
218
|
+
run: |
|
|
219
|
+
# An apply that started while we pulled may have left a half-applied
|
|
220
|
+
# state in the files — drop this round rather than propose it.
|
|
221
|
+
if [ "$(gh run list --workflow datum-space.yml --limit 20 --json status,event --jq '[.[] | select(.event != "pull_request_target" and .status != "completed")] | length')" != "0" ]; then
|
|
222
|
+
echo "An apply started during the pull — skipping; the next sync sees the result."
|
|
223
|
+
exit 0
|
|
224
|
+
fi
|
|
225
|
+
git add -A ${DIR}
|
|
226
|
+
if git diff --cached --quiet; then
|
|
227
|
+
echo "The repository already matches the space."
|
|
228
|
+
exit 0
|
|
229
|
+
fi
|
|
230
|
+
# ${DIR}/ already on main = drift; absent = the first import.
|
|
231
|
+
if [ -n "$(git ls-tree -d --name-only HEAD ${DIR})" ]; then
|
|
232
|
+
title="Sync z przestrzeni: zmiany spoza repozytorium"
|
|
233
|
+
body="Konfiguracja przestrzeni ${space} różni się od repozytorium — ktoś zmienił ją w panelu albo innym narzędziem. Zmerguj, żeby zachować zmianę. Żeby ją cofnąć: zamknij i uruchom workflow Datum space na main (Run workflow) — przywróci stan z repozytorium."
|
|
234
|
+
else
|
|
235
|
+
title="Import konfiguracji przestrzeni ${space}"
|
|
236
|
+
body="Pierwszy import: stan przestrzeni ${space} jako pliki w ${DIR}/. Po merge apply nie powinien nic zmienić (plan = same noop)."
|
|
237
|
+
fi
|
|
238
|
+
git config user.name "datum-space-sync"
|
|
239
|
+
git config user.email "github-actions@users.noreply.github.com"
|
|
240
|
+
git checkout -B ${SPACE_REPO_SYNC_BRANCH}
|
|
241
|
+
git commit -m "$title"
|
|
242
|
+
git push --force origin ${SPACE_REPO_SYNC_BRANCH}
|
|
243
|
+
if [ -z "$(gh pr list --head ${SPACE_REPO_SYNC_BRANCH} --state open --json number --jq '.[].number')" ]; then
|
|
244
|
+
gh pr create --head ${SPACE_REPO_SYNC_BRANCH} --title "$title" --body "$body" || {
|
|
245
|
+
echo "::error::Could not open the pull request. Turn on Settings → Actions → General → \\"Allow GitHub Actions to create and approve pull requests\\" (off by default), then re-run. The branch ${SPACE_REPO_SYNC_BRANCH} is pushed — a person can open the pull request from it meanwhile."
|
|
246
|
+
exit 1
|
|
247
|
+
}
|
|
248
|
+
fi
|
|
249
|
+
`;
|
|
250
|
+
}
|
|
251
|
+
function agentsMd(space) {
|
|
252
|
+
return `# Working in the Datum space repository \`${space}\`
|
|
253
|
+
|
|
254
|
+
This repository holds the configuration of the Datum space \`${space}\` as
|
|
255
|
+
files. \`main\` is what the space runs: a merge applies it through the space
|
|
256
|
+
manifest. There is no Terraform and no state file — the server plans every
|
|
257
|
+
change against the live space.
|
|
258
|
+
|
|
259
|
+
## Layout (\`${DIR}/\`)
|
|
260
|
+
|
|
261
|
+
| Path | What it is |
|
|
262
|
+
| --- | --- |
|
|
263
|
+
| \`${DIR}/space.json\` | \`apiVersion\` + scalar kinds (\`autonomyOptIn\`, \`skillsRepoUrl\`) |
|
|
264
|
+
| \`${DIR}/<kind>.json\` | a map kind: \`settings\`, \`github\`, \`jira\`, \`teams\`, \`victorops\`, \`grafanaAlerts\`, \`credentialBindings\` |
|
|
265
|
+
| \`${DIR}/<kind>/<entry>.json\` | one entry of a list kind: \`agents\`, \`routines\`, \`flows\`, \`queues\`, \`caseBoards\`, \`knowledgeBases\`, \`credentials\`, … |
|
|
266
|
+
| \`${DIR}/<kind>/<entry>.prompt.md\` | the entry's prompt (agents, routines) |
|
|
267
|
+
| \`${DIR}/<kind>/<entry>.<field>.json\` | a large JSON field: routine \`inputSchema\`/\`outputSchema\`, flow \`definition\` |
|
|
268
|
+
|
|
269
|
+
An extracted field is referenced from its entry as \`{ "$file": "<name>" }\`.
|
|
270
|
+
Every kind's fields, with descriptions: \`datum space-manifest schema <kind>
|
|
271
|
+
--space ${space}\` (no kind = the whole schema). Every \`datum\` command below
|
|
272
|
+
names the space with \`--space ${space}\`: a sign-in addresses the
|
|
273
|
+
organisation's host otherwise, where a space member has no authority.
|
|
274
|
+
|
|
275
|
+
## Creating something new (an agent, a routine, a flow, a queue…)
|
|
276
|
+
|
|
277
|
+
1. **Take the shape from Datum, nowhere else.** \`datum space-manifest schema
|
|
278
|
+
<kind> --space ${space}\` lists the kind's fields and what each means. For a
|
|
279
|
+
flow, look in the flow catalog first (\`datum flow-catalog entries list
|
|
280
|
+
--space ${space}\`, then \`datum flow-catalog entries get <id> --space ${space}\`)
|
|
281
|
+
— flows other spaces published and the organisation certified; one that
|
|
282
|
+
fits is installed through \`flowInstalls\`, not copied — and read the step
|
|
283
|
+
types in \`datum flows create --help\`. An entry already in \`${DIR}/\` is a
|
|
284
|
+
good second example.
|
|
285
|
+
2. **Do not read other spaces' repositories, other spaces' configuration or
|
|
286
|
+
the \`datum-core\` source** to learn a shape. They are other teams' local
|
|
287
|
+
decisions, with queues, credentials and people that do not exist here,
|
|
288
|
+
and source code is not the contract — the schema, the catalog and
|
|
289
|
+
\`--help\` are.
|
|
290
|
+
3. **Ask the space what it is for.** \`datum a2a send --space ${space} "…"\` hands
|
|
291
|
+
a question to the space's own agent: which routine already does this,
|
|
292
|
+
which queue triages it, who approves.
|
|
293
|
+
4. **People by nick.** A routine's \`owner\`, an approver, a queue member: a
|
|
294
|
+
person of this space — \`datum space-members list --space ${space}\` (the
|
|
295
|
+
\`userId\` of a row is the nick; \`derived\` lists the people a bound Entra
|
|
296
|
+
group admits). Never an e-mail or an id.
|
|
297
|
+
5. **Check before the pull request.** \`datum space-manifest validate ${DIR}
|
|
298
|
+
--space ${space}\` must pass (any member can run it: for a space admin it is the dry
|
|
299
|
+
run's full check, below admin the shape only). It calls the server, so it
|
|
300
|
+
needs a sign-in (\`datum login\`) or a key in \`DATUM_API_KEY\` — like \`schema\`;
|
|
301
|
+
offline or signed out both answer "not signed in". A space
|
|
302
|
+
admin also sees the plan against the live space: \`datum space-manifest
|
|
303
|
+
push ${DIR} --dry-run --space ${space}\`.
|
|
304
|
+
|
|
305
|
+
A minimal new routine is two files — the entry, named after its \`name\`
|
|
306
|
+
(lower-case, dashes; \`pull\` keeps that name), and its prompt next to it:
|
|
307
|
+
|
|
308
|
+
\`${DIR}/routines/raport-dnia.json\`
|
|
309
|
+
|
|
310
|
+
\`\`\`json
|
|
311
|
+
{
|
|
312
|
+
"name": "Raport dnia",
|
|
313
|
+
"prompt": { "$file": "raport-dnia.prompt.md" },
|
|
314
|
+
"owner": "<nick from datum space-members list>",
|
|
315
|
+
"cron": "0 7 * * 1-5",
|
|
316
|
+
"timezone": "Europe/Warsaw"
|
|
317
|
+
}
|
|
318
|
+
\`\`\`
|
|
319
|
+
|
|
320
|
+
\`${DIR}/routines/raport-dnia.prompt.md\` — the prompt itself, in Polish. No
|
|
321
|
+
\`cron\` (or \`""\`) = the routine runs on demand only; every other field
|
|
322
|
+
(\`inputSchema\`, \`triggers\`, \`runsAs\`, …) is in \`datum space-manifest
|
|
323
|
+
schema routines --space ${space}\`.
|
|
324
|
+
|
|
325
|
+
## Rules
|
|
326
|
+
|
|
327
|
+
1. **Change files, never the space directly.** Edit under \`${DIR}/\`, open a
|
|
328
|
+
pull request, read the plan the CI comments, merge. An edit made in the
|
|
329
|
+
panel comes back as a "Sync z przestrzeni" pull request.
|
|
330
|
+
2. **\`main\` applies.** Plan locally as much as you like (\`push --dry-run\`, a
|
|
331
|
+
space admin's sign-in is enough). Apply from a workstation (\`push\`
|
|
332
|
+
without \`--dry-run\`) only when the person asks for it AND the same change
|
|
333
|
+
is on its way to \`main\` — otherwise the next apply on \`main\` reverts it.
|
|
334
|
+
3. **No secrets, ever.** A credential is declared by \`label\` + \`type\`; a
|
|
335
|
+
person fills its material in the panel. \`push\` refuses strings in common
|
|
336
|
+
key and token formats — a safety net, not a guarantee. Never print
|
|
337
|
+
\`DATUM_API_KEY\`.
|
|
338
|
+
4. **The repository mirrors the whole space; deleting is explicit.** The
|
|
339
|
+
sync writes every entry the space has. Removing a file does NOT delete
|
|
340
|
+
the entry: the plan reports it as \`orphan\`. To delete: remove the file
|
|
341
|
+
in a pull request, merge, then run **Actions → Datum space → Run workflow**
|
|
342
|
+
on \`main\` with **prune** ticked (it deletes only within kinds \`${DIR}/\`
|
|
343
|
+
still declares). The panel refuses deleting some entries the repository
|
|
344
|
+
manages (a routine answers 409) — the repository is the way.
|
|
345
|
+
5. **A new entry gets its defaults from the server.** The sync after the
|
|
346
|
+
merge brings the entry back with every field filled in (and a file name
|
|
347
|
+
derived from its key) — merge that pull request; it is not a panel edit.
|
|
348
|
+
6. **Organisation-tier kinds do not live here** (\`features\`,
|
|
349
|
+
\`monthlyBudgetUsd\`, \`allocation\`, \`members\`, grants) — the org admin
|
|
350
|
+
decides them. A kind whose feature is off for the organisation is
|
|
351
|
+
refused by the plan: ask an org admin.
|
|
352
|
+
7. **Natural keys, never ids**: agents by nick (\`id\`), routines and flows by
|
|
353
|
+
\`name\`, credentials by \`label\`; a flow step names its routine by name.
|
|
354
|
+
8. **Domain content is Polish** — prompts, descriptions. Keys and file names
|
|
355
|
+
stay as \`pull\` wrote them.
|
|
356
|
+
9. **Write values the way the server stores them.** When "Verify
|
|
357
|
+
convergence" fails after an apply, the server normalised a value: copy
|
|
358
|
+
the form the plan shows into the file.
|
|
359
|
+
|
|
360
|
+
## Identity
|
|
361
|
+
|
|
362
|
+
\`datum login\` signs a person in with their own role in the space — the same
|
|
363
|
+
as in the panel (\`datum whoami --space ${space}\` shows it). Any member can read
|
|
364
|
+
the schema and validate; a space admin can also pull and plan from a
|
|
365
|
+
workstation. The CI plans and applies with \`${SPACE_REPO_KEY_SECRET}\`, a key
|
|
366
|
+
bound to this space that lives only in the repository's secrets.
|
|
367
|
+
|
|
368
|
+
The \`datum\` Claude Code plugin gives an agent the space's tools as the
|
|
369
|
+
signed-in person; \`datum a2a send --space ${space} "…"\` hands a question to
|
|
370
|
+
the space's own agent — ask it what a routine or a flow is for before
|
|
371
|
+
changing it.`;
|
|
372
|
+
}
|
|
373
|
+
/** The markers around the part of AGENTS.md / CLAUDE.md Datum owns (#1824):
|
|
374
|
+
* a second \`datum space-manifest init\` replaces what is between them with the
|
|
375
|
+
* current text — the team's own notes outside them stay. */
|
|
376
|
+
export const SPACE_REPO_MANAGED_BEGIN = "<!-- datum:managed:begin — written by `datum space-manifest init`; a re-run replaces this block. Add your own notes below the end marker. -->";
|
|
377
|
+
export const SPACE_REPO_MANAGED_END = "<!-- datum:managed:end -->";
|
|
378
|
+
function managedBlock(body) {
|
|
379
|
+
return `${SPACE_REPO_MANAGED_BEGIN}\n${body.trimEnd()}\n${SPACE_REPO_MANAGED_END}\n`;
|
|
380
|
+
}
|
|
381
|
+
/** The first line of an AGENTS.md written before the managed block existed
|
|
382
|
+
* (0.530.0) — such a file is Datum's whole, so a re-run replaces it. */
|
|
383
|
+
const LEGACY_AGENTS_TITLE = "# Working in the Datum space repository";
|
|
384
|
+
/**
|
|
385
|
+
* What a re-run of init writes into an existing AGENTS.md / CLAUDE.md:
|
|
386
|
+
* the block between the markers replaced; a pre-marker file Datum wrote
|
|
387
|
+
* whole (the legacy AGENTS.md, the one-line CLAUDE.md) replaced; any other
|
|
388
|
+
* file keeps its text and gets the block appended. `null` = already current.
|
|
389
|
+
*/
|
|
390
|
+
export function refreshManagedFile(existing, generated) {
|
|
391
|
+
const begin = existing.indexOf(SPACE_REPO_MANAGED_BEGIN);
|
|
392
|
+
const end = existing.indexOf(SPACE_REPO_MANAGED_END);
|
|
393
|
+
const block = generated.slice(0, generated.indexOf(SPACE_REPO_MANAGED_END) + SPACE_REPO_MANAGED_END.length);
|
|
394
|
+
let next;
|
|
395
|
+
if (begin !== -1 && end > begin) {
|
|
396
|
+
next = existing.slice(0, begin) + block + existing.slice(end + SPACE_REPO_MANAGED_END.length);
|
|
397
|
+
}
|
|
398
|
+
else if (existing.startsWith(LEGACY_AGENTS_TITLE) || existing.trim() === "@AGENTS.md") {
|
|
399
|
+
next = generated;
|
|
400
|
+
}
|
|
401
|
+
else {
|
|
402
|
+
next = `${existing.trimEnd()}\n\n${block}\n`;
|
|
403
|
+
}
|
|
404
|
+
return next === existing ? null : next;
|
|
405
|
+
}
|
|
406
|
+
/** Files of the repository Datum owns whole (the workflows, the line-ending
|
|
407
|
+
* rule): `init --refresh` overwrites them with the current version — e.g.
|
|
408
|
+
* to get the apply workflow's prune input into a repository set up before. */
|
|
409
|
+
export const SPACE_REPO_DATUM_OWNED = new Set([
|
|
410
|
+
".github/workflows/datum-space.yml",
|
|
411
|
+
".github/workflows/datum-space-sync.yml",
|
|
412
|
+
".gitattributes",
|
|
413
|
+
]);
|
|
414
|
+
/** Files with a managed block — every re-run of init refreshes the block. */
|
|
415
|
+
export const SPACE_REPO_MANAGED_BLOCK_FILES = new Set(["AGENTS.md", "CLAUDE.md"]);
|
|
416
|
+
function readme(space) {
|
|
417
|
+
return `# Datum space \`${space}\` — configuration as code
|
|
418
|
+
|
|
419
|
+
The agents, routines, flows, connectors and boards of the Datum space
|
|
420
|
+
\`${space}\`, kept as files in \`${DIR}/\` and applied on every merge to \`main\`.
|
|
421
|
+
|
|
422
|
+
## Changing the space
|
|
423
|
+
|
|
424
|
+
1. Edit files under \`${DIR}/\` (prompts are \`*.prompt.md\`) and open a pull request.
|
|
425
|
+
2. The CI comments the plan: what will be created, updated, left alone.
|
|
426
|
+
3. Merge — the CI applies it and checks that a second plan is empty.
|
|
427
|
+
|
|
428
|
+
An edit made in the panel comes back as a "Sync z przestrzeni" pull request
|
|
429
|
+
on the next weekday morning. Merge it to keep the edit. To revert it, close
|
|
430
|
+
the pull request and run **Actions → Datum space → Run workflow** on main —
|
|
431
|
+
that re-applies the repository (an apply otherwise runs only when a merge
|
|
432
|
+
touches \`${DIR}/\`).
|
|
433
|
+
|
|
434
|
+
## Setup (once)
|
|
435
|
+
|
|
436
|
+
| Where | What |
|
|
437
|
+
| --- | --- |
|
|
438
|
+
| Settings → Actions → General | **Allow GitHub Actions to create and approve pull requests** (off by default — the sync needs it) |
|
|
439
|
+
| Runners | the workflows run on \`[self-hosted, ubuntu-latest, ARM64]\` — the organisation's runner group must serve this repository |
|
|
440
|
+
| Repository variable \`${SPACE_REPO_BASE_URL_VAR}\` | the organisation host, e.g. \`https://datum.example.com\` |
|
|
441
|
+
| Repository secret \`${SPACE_REPO_KEY_SECRET}\` | a key bound to \`${space}\`, role **admin** — panel: Settings → API keys |
|
|
442
|
+
| First import | Actions → **Datum space sync** → Run workflow, then merge its pull request |
|
|
443
|
+
|
|
444
|
+
Rules for people and agents editing this repository: [AGENTS.md](AGENTS.md).
|
|
445
|
+
`;
|
|
446
|
+
}
|
|
447
|
+
/** Every file of a fresh space repository, keyed by its path. */
|
|
448
|
+
export function spaceRepoFiles(space) {
|
|
449
|
+
return {
|
|
450
|
+
".github/workflows/datum-space.yml": planApplyWorkflow(space),
|
|
451
|
+
".github/workflows/datum-space-sync.yml": syncWorkflow(space),
|
|
452
|
+
"AGENTS.md": `${managedBlock(agentsMd(space))}\n## Team notes\n\nYour team's own rules for this repository go here, below the managed block — \`datum space-manifest init\` keeps them.\n`,
|
|
453
|
+
"CLAUDE.md": managedBlock("@AGENTS.md"),
|
|
454
|
+
"README.md": readme(space),
|
|
455
|
+
".gitignore": "plan.md\napply.md\nnode_modules/\n.DS_Store\n",
|
|
456
|
+
// Git for Windows checks text out with CRLF by default (core.autocrlf):
|
|
457
|
+
// a prompt file would then carry \r on every line and plan as changed.
|
|
458
|
+
// The manifest files stay LF on every machine, byte for byte as `pull`
|
|
459
|
+
// wrote them.
|
|
460
|
+
".gitattributes": `${DIR}/** text eol=lf\n.github/workflows/** text eol=lf\n`,
|
|
461
|
+
};
|
|
462
|
+
}
|
|
463
|
+
//# sourceMappingURL=spaceRepoTemplate.js.map
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `datum update` (#1512) — the foreground face of selfUpdate.ts.
|
|
3
|
+
*
|
|
4
|
+
* datum update install the release the server runs, when newer
|
|
5
|
+
* datum update --check what runs here, what the server offers, the mode
|
|
6
|
+
* datum update --to <version> switch to an installed version (rollback)
|
|
7
|
+
* datum update --to package back to the npm-installed package
|
|
8
|
+
* datum update --auto on|off the person's opt-out (DATUM_AUTOUPDATE wins over it)
|
|
9
|
+
* datum update --background the detached check the launcher starts (silent)
|
|
10
|
+
*/
|
|
11
|
+
import { checkBundleSignature } from "./bundleSignature.js";
|
|
12
|
+
import { AUTOUPDATE_MIN_ONLY, AUTOUPDATE_OFF, CLI_PACKAGE_NAME, CLI_PACKAGE_NAMES, ENV_LAUNCHER_VERSION, autoUpdateMode, checkForUpdate, clearCurrent, compareVersions, describeWhen, effectiveVersion, fetchLatest, installDeferredUntil, installedVersions, isPackagedInstall, oneLine, readCurrent, readState, switchTo, takeNotice, writeState, } from "./selfUpdate.js";
|
|
13
|
+
const KNOWN_FLAGS = new Set(["check", "to", "auto", "background"]);
|
|
14
|
+
function parse(args) {
|
|
15
|
+
const flags = new Map();
|
|
16
|
+
for (let i = 0; i < args.length; i++) {
|
|
17
|
+
const t = args[i];
|
|
18
|
+
if (!t.startsWith("--"))
|
|
19
|
+
throw new Error(`unexpected argument \`${t}\` — see: datum update --help`);
|
|
20
|
+
const eq = t.indexOf("=");
|
|
21
|
+
const name = eq === -1 ? t.slice(2) : t.slice(2, eq);
|
|
22
|
+
if (!KNOWN_FLAGS.has(name))
|
|
23
|
+
throw new Error(`unknown flag --${name}. Known flags: --check, --to, --auto`);
|
|
24
|
+
if (name === "check" || name === "background") {
|
|
25
|
+
flags.set(name, "");
|
|
26
|
+
continue;
|
|
27
|
+
}
|
|
28
|
+
const value = eq === -1 ? args[++i] : t.slice(eq + 1);
|
|
29
|
+
if (value === undefined || value === "")
|
|
30
|
+
throw new Error(`--${name} needs a value`);
|
|
31
|
+
flags.set(name, value);
|
|
32
|
+
}
|
|
33
|
+
return flags;
|
|
34
|
+
}
|
|
35
|
+
/** The `--check` line after a failed install (#3325): whether — and when — it is tried again. */
|
|
36
|
+
function nextAttemptLine(state, latest, mode, running, now) {
|
|
37
|
+
if (mode === AUTOUPDATE_OFF)
|
|
38
|
+
return "→ automatic updates are off here; `datum update` retries now";
|
|
39
|
+
if (mode === AUTOUPDATE_MIN_ONLY && compareVersions(running, latest.minVersion) >= 0) {
|
|
40
|
+
return "→ min-only: no automatic attempt above the server's minimum; `datum update` retries now";
|
|
41
|
+
}
|
|
42
|
+
if (state.failedInstall?.integrity !== latest.package?.integrity) {
|
|
43
|
+
return "→ the server now serves a rebuilt bundle: the next automatic attempt tries it, `datum update` now";
|
|
44
|
+
}
|
|
45
|
+
const until = installDeferredUntil(state, latest, now);
|
|
46
|
+
return until !== null
|
|
47
|
+
? `→ next automatic attempt after ${describeWhen(until, now)}; \`datum update\` retries now`
|
|
48
|
+
: "→ the next automatic attempt may run now";
|
|
49
|
+
}
|
|
50
|
+
/** The `--check` line for the server's bundle signature (#2612) — checkable without downloading it. */
|
|
51
|
+
function describeSignature(version, pkg, rootKeys) {
|
|
52
|
+
try {
|
|
53
|
+
// Only these two fields: anything else a server put in `package` (a
|
|
54
|
+
// `version`, say) must not stand in for the release it announced.
|
|
55
|
+
const check = checkBundleSignature({ version, integrity: pkg.integrity, signature: pkg.signature }, { rootKeys, packageNames: CLI_PACKAGE_NAMES });
|
|
56
|
+
return check.signed ? `signed by Datum (root key ${check.rootKeyId})` : `UNSIGNED — ${check.reason}`;
|
|
57
|
+
}
|
|
58
|
+
catch (err) {
|
|
59
|
+
return `INVALID — ${err.message}; this CLI will not install it`;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
export async function runUpdate(ctx, io) {
|
|
63
|
+
let flags;
|
|
64
|
+
try {
|
|
65
|
+
flags = parse(ctx.args);
|
|
66
|
+
}
|
|
67
|
+
catch (err) {
|
|
68
|
+
io.err.write(`datum: ${err.message}\n`);
|
|
69
|
+
return 2;
|
|
70
|
+
}
|
|
71
|
+
const background = flags.has("background");
|
|
72
|
+
const packaged = isPackagedInstall(ctx.entryDir);
|
|
73
|
+
// Handed over by the npm-installed launcher? Then THAT is "the package".
|
|
74
|
+
const packageVersion = ctx.env[ENV_LAUNCHER_VERSION] ?? ctx.ownVersion;
|
|
75
|
+
if (flags.has("auto")) {
|
|
76
|
+
const v = flags.get("auto").toLowerCase();
|
|
77
|
+
if (v !== "on" && v !== "off") {
|
|
78
|
+
io.err.write("datum: --auto takes on or off\n");
|
|
79
|
+
return 2;
|
|
80
|
+
}
|
|
81
|
+
writeState(ctx.paths, { autoUpdate: v === "on" });
|
|
82
|
+
io.out.write(`Automatic updates ${v === "on" ? "enabled" : "disabled"} for this user (${ctx.paths.stateFile}).\n`);
|
|
83
|
+
const effective = autoUpdateMode(ctx.env, readState(ctx.paths));
|
|
84
|
+
if ((v === "on") !== (effective !== AUTOUPDATE_OFF)) {
|
|
85
|
+
io.out.write(`Note: the environment overrides it here — the effective mode is \`${effective}\`.\n`);
|
|
86
|
+
}
|
|
87
|
+
return 0;
|
|
88
|
+
}
|
|
89
|
+
if (flags.has("to")) {
|
|
90
|
+
const target = flags.get("to");
|
|
91
|
+
try {
|
|
92
|
+
if (target === "package") {
|
|
93
|
+
clearCurrent(ctx.paths);
|
|
94
|
+
io.out.write(`The next \`datum\` runs the npm-installed package (${packageVersion}).\n`);
|
|
95
|
+
}
|
|
96
|
+
else {
|
|
97
|
+
switchTo(ctx.paths, target);
|
|
98
|
+
io.out.write(`The next \`datum\` runs ${target}.\n`);
|
|
99
|
+
if (compareVersions(target, packageVersion) < 0) {
|
|
100
|
+
io.out.write(`Note: ${target} is older than the npm-installed package (${packageVersion}), which wins — use \`--to package\` or reinstall the package at ${target}.\n`);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
return 0;
|
|
104
|
+
}
|
|
105
|
+
catch (err) {
|
|
106
|
+
io.err.write(`datum: ${err.message}\n`);
|
|
107
|
+
return 1;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
if (!packaged) {
|
|
111
|
+
if (!background) {
|
|
112
|
+
io.err.write(`datum: this CLI runs from a repository build (the app image or a checkout), which never updates itself — only the published ${CLI_PACKAGE_NAME} package does.\n`);
|
|
113
|
+
}
|
|
114
|
+
return background ? 0 : 1;
|
|
115
|
+
}
|
|
116
|
+
if (!ctx.baseUrl) {
|
|
117
|
+
if (!background) {
|
|
118
|
+
io.err.write("datum: no server to ask — pass --base-url, set DATUM_BASE_URL or run `datum login` first\n");
|
|
119
|
+
}
|
|
120
|
+
return background ? 0 : 2;
|
|
121
|
+
}
|
|
122
|
+
if (flags.has("check")) {
|
|
123
|
+
const state = readState(ctx.paths);
|
|
124
|
+
const lines = [
|
|
125
|
+
`running: ${effectiveVersion(ctx.ownVersion, ctx.paths)}`,
|
|
126
|
+
`package: ${packageVersion} (npm-installed)`,
|
|
127
|
+
`pointer: ${readCurrent(ctx.paths) ?? "(none)"}`,
|
|
128
|
+
`installed: ${installedVersions(ctx.paths).join(", ") || "(none)"}`,
|
|
129
|
+
`mode: ${autoUpdateMode(ctx.env, state)}`,
|
|
130
|
+
];
|
|
131
|
+
try {
|
|
132
|
+
const latest = await fetchLatest(ctx.baseUrl, ctx.deps.fetch, 10_000);
|
|
133
|
+
writeState(ctx.paths, { lastCheckAt: new Date(ctx.deps.now()).toISOString(), latest, lastError: undefined });
|
|
134
|
+
lines.push(`server: ${latest.version} (minimum ${latest.minVersion}${latest.package ? "" : ", no bundle to install"}) at ${ctx.baseUrl}`);
|
|
135
|
+
if (latest.package)
|
|
136
|
+
lines.push(`signature: ${describeSignature(latest.version, latest.package, ctx.deps.rootKeys)}`);
|
|
137
|
+
const running = effectiveVersion(ctx.ownVersion, ctx.paths);
|
|
138
|
+
lines.push(compareVersions(latest.version, running) > 0
|
|
139
|
+
? `→ ${latest.version} is available: run \`datum update\``
|
|
140
|
+
: "→ up to date");
|
|
141
|
+
const failed = state.failedInstall;
|
|
142
|
+
if (failed && latest.package && failed.version === latest.version && compareVersions(latest.version, running) > 0) {
|
|
143
|
+
lines.push(`failed: installing ${failed.version} failed ${failed.count}× (last ${failed.at}) — ${failed.error}`, nextAttemptLine(state, latest, autoUpdateMode(ctx.env, state), running, ctx.deps.now()));
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
catch (err) {
|
|
147
|
+
lines.push(`server: unreachable — ${err.message}`);
|
|
148
|
+
}
|
|
149
|
+
io.out.write(`${lines.join("\n")}\n`);
|
|
150
|
+
return 0;
|
|
151
|
+
}
|
|
152
|
+
try {
|
|
153
|
+
const before = effectiveVersion(ctx.ownVersion, ctx.paths);
|
|
154
|
+
const outcome = await checkForUpdate({
|
|
155
|
+
baseUrl: ctx.baseUrl,
|
|
156
|
+
ownVersion: ctx.ownVersion,
|
|
157
|
+
paths: ctx.paths,
|
|
158
|
+
deps: ctx.deps,
|
|
159
|
+
// A person asking for the update now gets an attempt now (#3325).
|
|
160
|
+
ignoreBackoff: !background,
|
|
161
|
+
});
|
|
162
|
+
if (background)
|
|
163
|
+
return 0;
|
|
164
|
+
if (outcome.installed) {
|
|
165
|
+
io.out.write(`Updated ${before} → ${outcome.installed}. The next \`datum\` runs it.\n`);
|
|
166
|
+
const notice = takeNotice(ctx.paths);
|
|
167
|
+
if (notice)
|
|
168
|
+
io.err.write(notice);
|
|
169
|
+
}
|
|
170
|
+
else if (!outcome.latest.package && compareVersions(outcome.latest.version, before) > 0) {
|
|
171
|
+
io.out.write(`The server runs ${outcome.latest.version} but carries no CLI bundle — install it with \`npm i -g ${CLI_PACKAGE_NAME}@${outcome.latest.version}\`.\n`);
|
|
172
|
+
}
|
|
173
|
+
else {
|
|
174
|
+
io.out.write(`datum ${before} is up to date (the server runs ${outcome.latest.version}).\n`);
|
|
175
|
+
}
|
|
176
|
+
return 0;
|
|
177
|
+
}
|
|
178
|
+
catch (err) {
|
|
179
|
+
if (!background)
|
|
180
|
+
io.err.write(`datum: the update failed — ${oneLine(err.message)}\n`);
|
|
181
|
+
return background ? 0 : 1;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
//# sourceMappingURL=updateCommand.js.map
|