dreamteamer 0.27.0 → 0.29.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/README.md +3 -0
- package/collections/collections.collection.yaml +23 -0
- package/collections/containers.collection.yaml +81 -0
- package/collections/images.collection.yaml +46 -0
- package/package.json +4 -3
- package/skills/using-dreamteamer/SKILL.md +1 -0
- package/skills/using-dreamteamer/references/getting-started.md +21 -0
- package/src/cli.js +96 -12
- package/src/collections-cli.js +10 -0
- package/src/commit.js +3 -2
- package/src/compile.js +115 -5
- package/src/containers.js +492 -0
- package/src/harnesses.js +57 -9
- package/src/init.js +4 -1
- package/src/ref.js +28 -6
- package/src/schema-ops.js +1 -0
- package/src/server.js +28 -0
- package/src/workspace.js +50 -0
package/README.md
CHANGED
|
@@ -150,6 +150,9 @@ adapters: Claude Code, Codex, Pi, Gemini CLI, Cursor. Author a skill once; every
|
|
|
150
150
|
|
|
151
151
|
## The editor
|
|
152
152
|
|
|
153
|
+
Extension id `dreamteamer.dreamteamer-vscode` (Marketplace · Open VSX). `init` and `compile` write the
|
|
154
|
+
`.vscode/extensions.json` recommendation, and `dt status` reports whether it is active.
|
|
155
|
+
|
|
153
156
|
[dreamteamer-vscode](https://github.com/dreamteamer/dreamteamer-vscode) gives you tables, boards,
|
|
154
157
|
calendars, maps, forms and a data-model designer over the same files — and it loads **the engine your
|
|
155
158
|
workspace pins**, so the editor, the CLI and any agent session are provably running the same code.
|
|
@@ -4,10 +4,24 @@ id: { generate: "{{ name | slug }}" }
|
|
|
4
4
|
schema:
|
|
5
5
|
type: object
|
|
6
6
|
required: [name, schema]
|
|
7
|
+
# The fields fall into five groups, in this order: IDENTITY (name · singular · title ·
|
|
8
|
+
# title_template — what the collection and a record of it are called), RETRIEVAL (description ·
|
|
9
|
+
# use_when — what brings a session here), SHAPE (extends · schema · id · sensitive), PLACEMENT
|
|
10
|
+
# (storage · module · group — where records live and who owns them), PRESENTATION (order ·
|
|
11
|
+
# list_fields · sort_field · icon · ui — how the surfaces show it). A new field joins one of
|
|
12
|
+
# these; a field that fits none is a sign it belongs on a record, not on the collection.
|
|
7
13
|
properties:
|
|
8
14
|
name:
|
|
9
15
|
type: string
|
|
10
16
|
description: The collection id — must equal the filename, and must be unique across every installed module.
|
|
17
|
+
singular:
|
|
18
|
+
type: string
|
|
19
|
+
description: >-
|
|
20
|
+
The word the CLI accepts beside `name` — `dt add task …` for `tasks`. DERIVED by inflection
|
|
21
|
+
when absent (`tasks` → `task`, `companies` → `company`, `rnd/projects` → `rnd/project`) and
|
|
22
|
+
authored only where inflection is wrong (`people` → `person`, `meeting-analyses` →
|
|
23
|
+
`meeting-analysis`). Typed input only: a REFERENCE inside a record still spells the full
|
|
24
|
+
name. compile refuses two collections whose name or singular coincide.
|
|
11
25
|
description:
|
|
12
26
|
type: string
|
|
13
27
|
description: >-
|
|
@@ -53,6 +67,15 @@ schema:
|
|
|
53
67
|
have lived in `modules/<module>/<kind>/` since the 2026-08-05 flatten, and a `system/`
|
|
54
68
|
prefix is only how `runtime.js` recognises a RUNTIME collection in a descriptor compiled
|
|
55
69
|
by a pre-flatten engine.
|
|
70
|
+
driver:
|
|
71
|
+
type: string
|
|
72
|
+
description: >-
|
|
73
|
+
The name of an engine DRIVER that answers this collection's verbs instead of a folder of
|
|
74
|
+
files — `docker` is the one shipped (src/containers.js: `containers`, `images`). A driver
|
|
75
|
+
collection has no records on disk: its derived `path` names a folder that never exists,
|
|
76
|
+
so check, commit and the store read zero records and never write one; the CLI and the REST
|
|
77
|
+
route dispatch to the driver first. A string checked against the drivers the engine has,
|
|
78
|
+
not an enum — one implementation, and a second is a code change, not a vocabulary change.
|
|
56
79
|
codec:
|
|
57
80
|
type: string
|
|
58
81
|
enum: [md, yaml, json, file]
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
name: containers
|
|
2
|
+
description: >-
|
|
3
|
+
A workspace running as a container — one person's editor, harness and compiled workspace over a
|
|
4
|
+
named volume set, answered over the Docker Engine API rather than from a folder of files.
|
|
5
|
+
use_when: >-
|
|
6
|
+
a workspace has to RUN somewhere for someone — `dt start container <name> --template <t>` makes
|
|
7
|
+
one and prints its editor URL; `dt list containers` says what is running and where; `stop`, `open`,
|
|
8
|
+
`rm` are the lifecycle. Not a record: nothing lands under data/, nothing is committed, and Docker
|
|
9
|
+
keeps the history
|
|
10
|
+
# MACHINERY WITH A DRIVER, NOT A RECORD COLLECTION. `storage.driver: docker` says every verb on this
|
|
11
|
+
# collection is answered by src/containers.js over the Engine API: `list` is GET /containers/json,
|
|
12
|
+
# `get` is an inspect, `add`/`start` create-and-start, `rm` removes. The derived storage.path names a
|
|
13
|
+
# folder that never exists, so every walker (check, commit, the store's index) reads zero records here
|
|
14
|
+
# and never writes one; the REST route and the CLI dispatch to the driver before the store is asked.
|
|
15
|
+
# `group: system` folds it out of the orientation block's domain listing, beside `repos` and the
|
|
16
|
+
# compiled kinds, which is where a stranger's mental model puts "the thing my workspace runs in".
|
|
17
|
+
storage:
|
|
18
|
+
driver: docker
|
|
19
|
+
schema:
|
|
20
|
+
type: object
|
|
21
|
+
required: [name, template]
|
|
22
|
+
properties:
|
|
23
|
+
# ---- identity ----
|
|
24
|
+
name:
|
|
25
|
+
type: string
|
|
26
|
+
description: The workspace name — also the container name and the stem of its three volumes (`dreamteamer-<name>-workspace` · `-home` · `-files`).
|
|
27
|
+
template:
|
|
28
|
+
type: string
|
|
29
|
+
description: The template it was made from — an image carrying `dreamteamer.template`, `dreamteamer.ports` and `dreamteamer.modules` labels; `--template hq` resolves to `<DT_REGISTRY>/hq:<DT_TEMPLATE_TAG>` or to `DT_IMAGE_hq`.
|
|
30
|
+
image:
|
|
31
|
+
type: string
|
|
32
|
+
description: The image reference the container runs.
|
|
33
|
+
id:
|
|
34
|
+
type: string
|
|
35
|
+
description: Docker's short id.
|
|
36
|
+
# ---- runtime ----
|
|
37
|
+
state:
|
|
38
|
+
type: string
|
|
39
|
+
enum: [created, running, paused, restarting, exited, dead]
|
|
40
|
+
description: Docker's container state.
|
|
41
|
+
status:
|
|
42
|
+
type: string
|
|
43
|
+
description: Docker's own phrase — `Up 2 hours`, `Exited (0) 3 minutes ago`.
|
|
44
|
+
started:
|
|
45
|
+
type: string
|
|
46
|
+
format: date-time
|
|
47
|
+
description: When it last started.
|
|
48
|
+
# ---- network ----
|
|
49
|
+
editor_url:
|
|
50
|
+
type: string
|
|
51
|
+
description: Where the person opens the editor — `http://localhost:<port>/?folder=<workspace dir>`; the host port comes from DT_PORT_BASE upward, bound to DT_BIND (127.0.0.1).
|
|
52
|
+
port:
|
|
53
|
+
type: integer
|
|
54
|
+
description: The host port the editor is published on.
|
|
55
|
+
bind:
|
|
56
|
+
type: string
|
|
57
|
+
description: The host address it is bound to — loopback unless the host .env says otherwise.
|
|
58
|
+
# ---- storage ----
|
|
59
|
+
workspace_dir:
|
|
60
|
+
type: string
|
|
61
|
+
description: Where the workspace is mounted inside — `/workspaces/<name>`, the dev-container convention.
|
|
62
|
+
volumes:
|
|
63
|
+
type: object
|
|
64
|
+
description: The three named volumes — `workspace` (the repo), `home` (the person's logins and editor settings), `files` (FILES_FOLDER). Plain `rm` keeps them; `rm --force` removes them.
|
|
65
|
+
mounts:
|
|
66
|
+
type: array
|
|
67
|
+
items: { type: string }
|
|
68
|
+
description: Extra bind or volume mounts passed as `--mount <host-path|volume>:<container-path>[:ro]`.
|
|
69
|
+
# ---- person ----
|
|
70
|
+
person:
|
|
71
|
+
type: string
|
|
72
|
+
description: Whose container this is — the git identity injected as env; the harness login happens INSIDE, once, and is never injected.
|
|
73
|
+
# ---- provenance ----
|
|
74
|
+
created:
|
|
75
|
+
type: string
|
|
76
|
+
format: date-time
|
|
77
|
+
description: When Docker created it.
|
|
78
|
+
order: 146
|
|
79
|
+
list_fields: [name, template, state, editor_url, person]
|
|
80
|
+
icon: deployed_code
|
|
81
|
+
group: system
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
name: images
|
|
2
|
+
description: >-
|
|
3
|
+
A template a workspace container is made from — a Docker image carrying the `dreamteamer.template`,
|
|
4
|
+
`dreamteamer.ports` and `dreamteamer.modules` labels — as the local Docker holds it.
|
|
5
|
+
use_when: >-
|
|
6
|
+
choosing what a container runs — `dt list images` shows the templates present, `dt add image
|
|
7
|
+
--template <t>` pulls one, `dt rm image <ref>` removes one; a template is an IMAGE WITH LABELS, not
|
|
8
|
+
a record here, so there is nothing to author — publish an image with the labels and it appears
|
|
9
|
+
# The second driver-backed collection (see containers.collection.yaml for what that means). `list` is
|
|
10
|
+
# GET /images/json filtered to the template label, `get` an inspect, `add --template` a pull, `rm` a
|
|
11
|
+
# delete. No records, no folder, no commit.
|
|
12
|
+
storage:
|
|
13
|
+
driver: docker
|
|
14
|
+
schema:
|
|
15
|
+
type: object
|
|
16
|
+
required: [image]
|
|
17
|
+
properties:
|
|
18
|
+
# ---- identity ----
|
|
19
|
+
image:
|
|
20
|
+
type: string
|
|
21
|
+
description: The image reference — `<registry>/<template>:<tag>`.
|
|
22
|
+
template:
|
|
23
|
+
type: string
|
|
24
|
+
description: The `dreamteamer.template` label — the word `--template` takes.
|
|
25
|
+
id:
|
|
26
|
+
type: string
|
|
27
|
+
description: Docker's image id.
|
|
28
|
+
# ---- what it runs ----
|
|
29
|
+
ports:
|
|
30
|
+
type: string
|
|
31
|
+
description: The `dreamteamer.ports` label — the in-container port the editor listens on (8080 by convention).
|
|
32
|
+
modules:
|
|
33
|
+
type: string
|
|
34
|
+
description: The `dreamteamer.modules` label — the modules the template installs, comma-separated.
|
|
35
|
+
# ---- size and time ----
|
|
36
|
+
size_mb:
|
|
37
|
+
type: integer
|
|
38
|
+
description: Image size in megabytes.
|
|
39
|
+
created:
|
|
40
|
+
type: string
|
|
41
|
+
format: date
|
|
42
|
+
description: When the image was built.
|
|
43
|
+
order: 147
|
|
44
|
+
list_fields: [template, image, size_mb, created]
|
|
45
|
+
icon: layers
|
|
46
|
+
group: system
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dreamteamer",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.29.0",
|
|
4
4
|
"description": "A workspace compiler for coding agents — schema-validated records as plain files over git, compiled into every harness",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"author": "Gilad Khen <giladkhen@gmail.com>",
|
|
@@ -29,7 +29,8 @@
|
|
|
29
29
|
"node": ">=20"
|
|
30
30
|
},
|
|
31
31
|
"bin": {
|
|
32
|
-
"dreamteamer": "bin/dreamteamer.js"
|
|
32
|
+
"dreamteamer": "bin/dreamteamer.js",
|
|
33
|
+
"dt": "bin/dreamteamer.js"
|
|
33
34
|
},
|
|
34
35
|
"files": [
|
|
35
36
|
"NOTICE",
|
|
@@ -49,7 +50,7 @@
|
|
|
49
50
|
"express": "^5.2.1",
|
|
50
51
|
"fractional-indexing": "^4.0.0",
|
|
51
52
|
"js-yaml": "^4.1.0",
|
|
52
|
-
"yaml": "2.8.
|
|
53
|
+
"yaml": "2.8.4"
|
|
53
54
|
},
|
|
54
55
|
"dreamteamer": {
|
|
55
56
|
"title": "System"
|
|
@@ -59,6 +59,7 @@ their flags, on one page (there is no per-verb `--help`).
|
|
|
59
59
|
the verb names, as a map (semantics and flags live in `help`; a test holds this list to the
|
|
60
60
|
dispatch, so it cannot drift):
|
|
61
61
|
|
|
62
|
+
- a collection may be spelled in the SINGULAR on any of these (`dt add task "call the bank"` — one bare positional is the title); references inside records still spell the full name
|
|
62
63
|
- read & measure — `list` `get` `values` `history` `diff` `next` `relations` `resolve`
|
|
63
64
|
- write & publish — `add` `set` `rm` `rename` `move` `revert` `commit`
|
|
64
65
|
- fields (sources, through the compile gate) — `add-field` `set-field` `rm-field` `rename-field` (system entities — modules, collections, skills, ui-views… — take the RECORD verbs above)
|
|
@@ -112,3 +112,24 @@ prefers. Nothing about the code moves; `check` and `compile` read only what desc
|
|
|
112
112
|
| hand-writing the first descriptor | `dt add collections` is compile-gated and publishes itself; hand-written sources owe `dt compile` |
|
|
113
113
|
| rewriting existing files to fit a guessed schema | describe reality, compile, `check` — then decide which violations are worth fixing in the data |
|
|
114
114
|
| waiting for a UI before starting | the CLI and the records are the complete system; any surface renders them later, unchanged |
|
|
115
|
+
|
|
116
|
+
## The editor
|
|
117
|
+
|
|
118
|
+
The VS Code-family extension is **`dreamteamer.dreamteamer-vscode`** (Marketplace and Open VSX). It
|
|
119
|
+
loads the engine the workspace pins, so the editor, the CLI and an agent session run the same code.
|
|
120
|
+
`init` writes `.vscode/extensions.json` recommending it and `compile` keeps that file current, so a
|
|
121
|
+
VS Code, Cursor or code-server window opened on the workspace offers to install it — that prompt is
|
|
122
|
+
the intended path. From a terminal:
|
|
123
|
+
|
|
124
|
+
```bash
|
|
125
|
+
code --install-extension dreamteamer.dreamteamer-vscode # VS Code (on some machines `code` is Cursor)
|
|
126
|
+
code-server --install-extension dreamteamer.dreamteamer-vscode \
|
|
127
|
+
--extensions-dir <the dir the running server was started with> # match its --extensions-dir, or the window never sees it
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
⚠ Inside code-server's own terminal (or an agent it started) that command fails with `error not spawned
|
|
131
|
+
with IPC`: the shell inherits code-server's `VSCODE_*` / `CODE_SERVER_PARENT_PID` variables and the
|
|
132
|
+
CLI thinks it is a forked child. Prefix it with `env -u VSCODE_ESM_ENTRYPOINT -u VSCODE_HANDLES_SIGPIPE
|
|
133
|
+
-u VSCODE_HANDLES_UNCAUGHT_ERRORS -u VSCODE_NLS_CONFIG -u VSCODE_CWD -u VSCODE_RECONNECTION_GRACE_TIME
|
|
134
|
+
-u CODE_SERVER_PARENT_PID`, or let the recommendation prompt do the install. Whether the extension is
|
|
135
|
+
ACTIVE is `dt status`'s `editor:` line — it reads the marker the extension writes on activation.
|
package/src/cli.js
CHANGED
|
@@ -16,6 +16,7 @@ import { findWorkspace } from './workspace.js';
|
|
|
16
16
|
import { compile, staleness, warnIfStale, discoverModules, CHANNEL_LABEL, locationOf, KINDS } from './compile.js';
|
|
17
17
|
import { check } from './check.js';
|
|
18
18
|
import { collectionCommand, emit, relationsCommand, parseArgs, refuseUnknownFlags } from './collections-cli.js';
|
|
19
|
+
import { driverTarget, driverCommand, setup as hostSetup, parseFlags as hostFlags, DRIVER_VERBS, LIFECYCLE_VERBS, CONTAINER_FLAGS } from './containers.js';
|
|
19
20
|
import { init, installClone, update, listRepos } from './init.js';
|
|
20
21
|
import { installCommand, describeCheckout, listWorktrees, worktreeCommand } from './checkout.js';
|
|
21
22
|
import { proveCommand, readLedger, flagEnabled } from './prove.js';
|
|
@@ -23,7 +24,7 @@ import { landCommand } from './land.js';
|
|
|
23
24
|
import { deriveEvents } from './events.js';
|
|
24
25
|
import { commitPending } from './commit.js';
|
|
25
26
|
import { Store } from './store.js';
|
|
26
|
-
import { splitRef } from './ref.js';
|
|
27
|
+
import { splitRef, canonicalCollection } from './ref.js';
|
|
27
28
|
import { envContext, renderTemplate } from './env-vars.js';
|
|
28
29
|
import { exportCommand, EXPORT_FLAGS } from './export-notebooklm.js';
|
|
29
30
|
|
|
@@ -57,8 +58,12 @@ the longest DECLARED collection prefix, so finance/transactions/2026/03/coffee i
|
|
|
57
58
|
case-insensitive variants; date-times sort and
|
|
58
59
|
compare as instants, across offsets)
|
|
59
60
|
get <collection>/<id> [--json]
|
|
60
|
-
add <collection> --<field> <value> … [--id <explicit-id>]
|
|
61
|
-
(
|
|
61
|
+
add <collection> ["<title>"] --<field> <value> … [--id <explicit-id>]
|
|
62
|
+
(ONE bare positional fills the collection's title
|
|
63
|
+
field — the one its title_template names — so
|
|
64
|
+
dt add task "call the bank" is
|
|
65
|
+
dt add tasks --name "call the bank".
|
|
66
|
+
a codec-file collection takes --from <path>
|
|
62
67
|
instead — the file IS the record, fields derive;
|
|
63
68
|
--force replaces an existing file record.
|
|
64
69
|
A repeated --<field> is one ELEMENT of an array
|
|
@@ -164,6 +169,12 @@ collection: the ENGINE does not read one, and \`rename-field\` was the only capa
|
|
|
164
169
|
title_template, id.generate, a ui-view's options.columns and filter,
|
|
165
170
|
and a command-binding's can-enter/can-exit. ONE commit)
|
|
166
171
|
|
|
172
|
+
Every <collection> above may be spelled in the SINGULAR: dt add task …, dt get task/<id>,
|
|
173
|
+
dt list meeting-analysis, dt add-field task …. The singular is derived from the descriptor
|
|
174
|
+
(tasks → task, companies → company) and authored on it as singular: where inflection is
|
|
175
|
+
wrong (people → person); compile refuses two collections whose words collide. A REFERENCE
|
|
176
|
+
inside a record still names the collection in full — tasks/kickoff, never task/kickoff.
|
|
177
|
+
|
|
167
178
|
Every verb that MOVES records or CLEARS values takes --dry-run and prints its plan first:
|
|
168
179
|
records N · refs M · descriptors K · values cleared V
|
|
169
180
|
|
|
@@ -232,6 +243,31 @@ workspace verbs:
|
|
|
232
243
|
and one \`proofs:\` line counting each proof's LAST verdict on this machine
|
|
233
244
|
[--strict] exit 1 when any proof's ledger tail is a FAIL
|
|
234
245
|
start serve the clean REST api at /api [--port <n>]
|
|
246
|
+
|
|
247
|
+
containers — a workspace as a running container (Docker Engine API over its socket, no dependency;
|
|
248
|
+
these verbs work with NO workspace, so npm i -g dreamteamer and Docker Desktop are enough):
|
|
249
|
+
setup make THIS MACHINE ready: checks Docker, writes ~/.dreamteamer/.env with its defaults
|
|
250
|
+
(DT_PORT_BASE 8100 · DT_BIND 127.0.0.1 · DT_REGISTRY · DT_TEMPLATE_TAG ·
|
|
251
|
+
DT_DOCKER_TIMEOUT 30 — seconds a request to Docker may sit idle before the verb
|
|
252
|
+
fails), lists the templates present, pulls one on request [--template <t>] [--json]
|
|
253
|
+
start container <name> --template <t> create-if-absent and start: a code-server editor at
|
|
254
|
+
http://localhost:<port>/?folder=/workspace over a compiled workspace, three named volumes
|
|
255
|
+
(workspace · home · files), image <DT_REGISTRY>/<template>:<tag> or DT_IMAGE_<template>.
|
|
256
|
+
The workspace is mounted at /workspaces/<name>. Idempotent. NO token is ever injected —
|
|
257
|
+
log in INSIDE, once; the home volume keeps it.
|
|
258
|
+
[--repo <git url>] clone an EXISTING workspace into the volume on first start, instead
|
|
259
|
+
of laying the template down — how a person joins one on GitHub
|
|
260
|
+
[--mount <host-path|volume>:<container-path>[:ro]] extra mounts, repeatable
|
|
261
|
+
[--name <git name>] [--email <git email>] [--no-open] [--json]
|
|
262
|
+
stop container <name> stop it; every volume kept [--json]
|
|
263
|
+
open container <name> print (and open) its editor URL [--no-open]
|
|
264
|
+
[--vscode] print (and open) the Dev Containers attach URI instead — the host's own
|
|
265
|
+
VS Code inside the container, extensions from the image's metadata label
|
|
266
|
+
list containers | images the record verbs, answered over Docker instead of a
|
|
267
|
+
get container <name> | image <ref> folder — singular or plural, either spelling.
|
|
268
|
+
rm container <name> [--force] plain rm keeps the volumes; --force removes them too
|
|
269
|
+
add image --template <t> pull a template's image; rm image <ref> removes one
|
|
270
|
+
|
|
235
271
|
changes what changed in every repo that holds records, as record events
|
|
236
272
|
[--since <sha|YYYY-MM-DD>] (default: HEAD~1 — the last commit's own changes) [--json]
|
|
237
273
|
commit publish records already written to disk: samples git status over every
|
|
@@ -286,7 +322,10 @@ export const GLOBAL_FLAGS = ['vault'];
|
|
|
286
322
|
export const WORKSPACE_FLAGS = {
|
|
287
323
|
init: ['name', 'data-path', 'harnesses', 'workspace-module'], update: [],
|
|
288
324
|
install: ['clone', 'dry-run', 'json', 'link-env', 'all', 'hook', 'print-adapters'],
|
|
289
|
-
start
|
|
325
|
+
// `start` is TWO forms: bare, the REST api (--port); with a `container <name>` target, the
|
|
326
|
+
// lifecycle verb — whose flags are the driver's. One table, because `flags-honoured` reads it.
|
|
327
|
+
start: ['port', ...CONTAINER_FLAGS], compile: ['watch'], check: [], status: ['strict'],
|
|
328
|
+
setup: ['template', 'json'], stop: ['json'], open: ['json', 'no-open', 'vscode'],
|
|
290
329
|
changes: ['since', 'json'], commit: ['dry-run', 'json'],
|
|
291
330
|
export: EXPORT_FLAGS,
|
|
292
331
|
// the UNION of every form's flags — the outer typo gate. Which flags each FORM takes is refused
|
|
@@ -299,6 +338,15 @@ export const WORKSPACE_FLAGS = {
|
|
|
299
338
|
export function run(argv) {
|
|
300
339
|
const [cmd, ...rest] = argv;
|
|
301
340
|
try {
|
|
341
|
+
// HOST VERBS resolve BEFORE workspace discovery: `setup`, and any verb whose target is a
|
|
342
|
+
// driver collection (`containers`, `images`, singular or plural). They answer identically on a
|
|
343
|
+
// bare machine — `npm i -g dreamteamer` and Docker Desktop, nothing else — and inside a
|
|
344
|
+
// workspace, because the thing they make IS the workspace (src/containers.js).
|
|
345
|
+
const host = hostDispatch(cmd, rest);
|
|
346
|
+
if (host) {
|
|
347
|
+
host.then((code) => process.exit(code)).catch((e) => { console.error(`✖ ${e.message}`); process.exit(1); });
|
|
348
|
+
return;
|
|
349
|
+
}
|
|
302
350
|
if (cmd in WORKSPACE_FLAGS) {
|
|
303
351
|
const bad = rest.filter((a) => a.startsWith('--')).map((a) => a.slice(2).split('=')[0]).find((f) => !WORKSPACE_FLAGS[cmd].includes(f));
|
|
304
352
|
if (bad) throw new Error(`unknown flag "--${bad}" on \`dt ${cmd}\`\n known: ${WORKSPACE_FLAGS[cmd].map((f) => `--${f}`).join(', ') || '(none — this verb takes no flags)'}`);
|
|
@@ -315,7 +363,9 @@ export function run(argv) {
|
|
|
315
363
|
for (let i = 0; i < rest.length; i++) if (rest[i].startsWith('--')) flags[rest[i].slice(2)] = rest[i + 1];
|
|
316
364
|
process.exit(init({ flags }));
|
|
317
365
|
}
|
|
318
|
-
if (!cmd) {
|
|
366
|
+
if (!cmd || cmd === 'help') {
|
|
367
|
+
// `help` works OUTSIDE a workspace too — the host verbs above do, and a person who just ran
|
|
368
|
+
// `npm i -g dreamteamer` on a bare machine has nothing else to read.
|
|
319
369
|
emit(USAGE);
|
|
320
370
|
process.exit(0);
|
|
321
371
|
}
|
|
@@ -507,6 +557,16 @@ export function run(argv) {
|
|
|
507
557
|
process.exit(1);
|
|
508
558
|
}
|
|
509
559
|
console.log(`compiled: ${s.manifest.compiled}`);
|
|
560
|
+
// THE EDITOR, from the marker the extension writes on activation (`.dreamteamer/editor.json`).
|
|
561
|
+
// Without it an agent working inside the editor could not tell whether the extension was
|
|
562
|
+
// installed, active, or refusing the engine — the operator had to report "no icon".
|
|
563
|
+
try {
|
|
564
|
+
const marker = path.join(ws.root, '.dreamteamer', 'editor.json');
|
|
565
|
+
if (fs.existsSync(marker)) {
|
|
566
|
+
const e = JSON.parse(fs.readFileSync(marker, 'utf8'));
|
|
567
|
+
console.log(`editor: ${e.extension ?? 'dreamteamer-vscode'} ${e.version ?? '?'} · ${e.state ?? 'active'} ${e.activated ?? ''} · engine ${e.engine ?? '?'}${e.host ? ` · ${e.host}` : ''}`);
|
|
568
|
+
} else console.log('editor: not detected — the extension dreamteamer.dreamteamer-vscode writes .dreamteamer/editor.json when it activates on this workspace');
|
|
569
|
+
} catch { console.log('editor: marker unreadable — .dreamteamer/editor.json is not JSON'); }
|
|
510
570
|
// provenance is LIVE discovery (not the manifest) — shows what the next compile would use
|
|
511
571
|
const { modules, shadows } = discoverModules(ws.root, ws.pkg);
|
|
512
572
|
const shadowed = new Map(shadows.map((sh) => [sh.name, sh]));
|
|
@@ -661,7 +721,8 @@ export function run(argv) {
|
|
|
661
721
|
if (!target || target.startsWith('--')) {
|
|
662
722
|
throw new Error(`dt ${cmd} needs a collection: dreamteamer ${cmd} <collection> --name <field> …`);
|
|
663
723
|
}
|
|
664
|
-
|
|
724
|
+
// the singular is legal here too: `dt add-field task --name due …`
|
|
725
|
+
process.exit(collectionCommand(ws, canonicalCollection(new Store(ws).descriptors, target) ?? target, cmd, flagArgs));
|
|
665
726
|
}
|
|
666
727
|
case 'relations':
|
|
667
728
|
warnIfStale(ws.root);
|
|
@@ -728,6 +789,25 @@ export function run(argv) {
|
|
|
728
789
|
}
|
|
729
790
|
}
|
|
730
791
|
|
|
792
|
+
/** The verbs that run with no workspace. Returns a promise of an exit code, or null when the
|
|
793
|
+
* command is not ours and the ordinary workspace dispatch should take it. */
|
|
794
|
+
function hostDispatch(cmd, rest) {
|
|
795
|
+
if (cmd === 'setup') {
|
|
796
|
+
const bad = rest.filter((a) => a.startsWith('--')).map((a) => a.slice(2).split('=')[0]).find((f) => !WORKSPACE_FLAGS.setup.includes(f));
|
|
797
|
+
if (bad) throw new Error(`unknown flag "--${bad}" on \`dt setup\`\n known: ${WORKSPACE_FLAGS.setup.map((f) => `--${f}`).join(', ')}`);
|
|
798
|
+
return hostSetup(hostFlags(rest).flags);
|
|
799
|
+
}
|
|
800
|
+
const target = driverTarget(rest[0]);
|
|
801
|
+
if (target && DRIVER_VERBS.has(cmd)) return driverCommand(cmd, target, rest.slice(1));
|
|
802
|
+
// A lifecycle verb aimed at anything else is refused by name: `dt start tasks` is not a
|
|
803
|
+
// server and not a container, and "unknown collection" would send the reader the wrong way.
|
|
804
|
+
if (LIFECYCLE_VERBS.has(cmd) && rest[0] && !rest[0].startsWith('--')) {
|
|
805
|
+
return Promise.reject(new Error(`\`${cmd}\` is a container lifecycle verb — "${rest[0]}" is not a container. dt ${cmd} container <name>${cmd === 'start' ? ' --template <t>' : ''}${cmd === 'start' ? '; a bare `dt start` serves the REST api' : ''}`));
|
|
806
|
+
}
|
|
807
|
+
if (cmd === 'stop' || cmd === 'open') return Promise.reject(new Error(`dt ${cmd} container <name> — see \`dreamteamer help\``));
|
|
808
|
+
return null;
|
|
809
|
+
}
|
|
810
|
+
|
|
731
811
|
/** Translate `dt <verb> <target> …` into the noun-verb call the implementation layer takes. */
|
|
732
812
|
function dispatchRecordVerb(ws, verb, args) {
|
|
733
813
|
const [target, ...rest] = args;
|
|
@@ -735,18 +815,22 @@ function dispatchRecordVerb(ws, verb, args) {
|
|
|
735
815
|
// A flag in the target slot is a word-order mistake, not a collection: without this,
|
|
736
816
|
// `dt list --json contacts` reported `unknown collection "--json"` and dumped every name.
|
|
737
817
|
if (target.startsWith('--')) throw new Error(`dt ${verb} takes its target BEFORE the flags: dreamteamer ${verb} <target> ${target} …`);
|
|
738
|
-
|
|
818
|
+
// A collection may be named by its declared name OR its singular (`dt add task …`); the
|
|
819
|
+
// canonical name is what every layer below sees. An unknown word passes through unchanged so
|
|
820
|
+
// the store's own "unknown collection" sentence, which lists what exists, is the one printed.
|
|
821
|
+
const { descriptors } = new Store(ws);
|
|
822
|
+
const canonical = canonicalCollection(descriptors, target) ?? target;
|
|
823
|
+
if (COLLECTION_VERBS.has(verb)) return collectionCommand(ws, canonical, verb, rest);
|
|
739
824
|
if (REF_VERBS.has(verb)) {
|
|
740
|
-
const { collection, id } = splitRef(
|
|
825
|
+
const { collection, id } = splitRef(descriptors, target);
|
|
741
826
|
return collectionCommand(ws, collection, verb, [id, ...rest]);
|
|
742
827
|
}
|
|
743
828
|
// EITHER_VERBS from here: a bare collection is legal for both — `move <collection> --init`,
|
|
744
829
|
// `next <collection>`.
|
|
745
|
-
|
|
746
|
-
if (descriptors.has(target)) {
|
|
830
|
+
if (descriptors.has(canonical)) {
|
|
747
831
|
return verb === 'move'
|
|
748
|
-
? collectionCommand(ws,
|
|
749
|
-
: collectionCommand(ws, 'commands', 'for', [
|
|
832
|
+
? collectionCommand(ws, canonical, 'move', rest)
|
|
833
|
+
: collectionCommand(ws, 'commands', 'for', [canonical, ...rest]);
|
|
750
834
|
}
|
|
751
835
|
const { collection, id } = splitRef(descriptors, target);
|
|
752
836
|
if (verb === 'move') return collectionCommand(ws, collection, 'move', [id, ...rest]);
|
package/src/collections-cli.js
CHANGED
|
@@ -179,6 +179,16 @@ export function collectionCommand(ws, collection, verb, args) {
|
|
|
179
179
|
return 0;
|
|
180
180
|
}
|
|
181
181
|
if (flags.from) throw new Error(`--from imports a file as a record, and "${collection}" is not a \`codec: file\` collection`);
|
|
182
|
+
// ONE bare positional is the record's title — the field `title_template` names — so
|
|
183
|
+
// `dt add task "call the bank"` reads as a sentence. Two positionals is a mistake (a flag
|
|
184
|
+
// value that lost its flag), and so is giving the title twice; both are refused by name.
|
|
185
|
+
if (pos.length > 1) throw new Error(`dt add ${collection} takes ONE positional (the title) and flags for the rest — got ${pos.length}: ${pos.map((p) => `"${p}"`).join(' ')}`);
|
|
186
|
+
if (pos.length === 1) {
|
|
187
|
+
const titleField = /\{\{\s*([A-Za-z_][\w]*)/.exec(d.title_template ?? '')?.[1];
|
|
188
|
+
if (!titleField || titleField === 'id') throw new Error(`"${collection}" labels its records by id, so there is no title field for "${pos[0]}" to fill — pass fields as --<field> <value>`);
|
|
189
|
+
if (titleField in flags) throw new Error(`the title was given twice — "${pos[0]}" and --${titleField} ${JSON.stringify(flags[titleField])}`);
|
|
190
|
+
flags[titleField] = pos[0];
|
|
191
|
+
}
|
|
182
192
|
const fields = coerceArrays(d, stripMeta(flags));
|
|
183
193
|
const { id, file, idFallback } = store.add(collection, fields, { id: flags.id });
|
|
184
194
|
flags.json
|
package/src/commit.js
CHANGED
|
@@ -7,7 +7,7 @@ import path from 'node:path';
|
|
|
7
7
|
import { pathToRecord } from './events.js';
|
|
8
8
|
import { parseRecordText } from './records.js';
|
|
9
9
|
import { relationsOf } from './relations.js';
|
|
10
|
-
import { splitRef } from './ref.js';
|
|
10
|
+
import { splitRef, canonicalCollection } from './ref.js';
|
|
11
11
|
|
|
12
12
|
// git calls whose failure we CATCH must not print git's own error: execFileSync forwards the
|
|
13
13
|
// child's stderr to ours unless told otherwise, so a handled "not a git repository" still
|
|
@@ -42,7 +42,8 @@ function parseTargets(descriptors, only) {
|
|
|
42
42
|
const whole = new Set();
|
|
43
43
|
const records = new Map();
|
|
44
44
|
for (const target of only) {
|
|
45
|
-
|
|
45
|
+
const asCollection = canonicalCollection(descriptors, target);
|
|
46
|
+
if (asCollection) { whole.add(asCollection); scope.add(asCollection); continue; }
|
|
46
47
|
const { collection, id } = splitRef(descriptors, target);
|
|
47
48
|
records.set(`${collection}/${id}`, { collection, id });
|
|
48
49
|
scope.add(collection);
|