@cybernetyx1/atlasflow-cli 0.1.0 → 0.1.2

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 CHANGED
@@ -5,7 +5,7 @@ The `atlasflow` command-line tool for scaffolding, running, building, testing, a
5
5
  ## Install
6
6
 
7
7
  ```sh
8
- pnpm add @cybernetyx1/atlasflow-cli
8
+ npm install @cybernetyx1/atlasflow-cli
9
9
  ```
10
10
 
11
11
  Part of the AtlasFlow monorepo. Proprietary.
@@ -15,9 +15,9 @@ Part of the AtlasFlow monorepo. Proprietary.
15
15
  Scaffold a workspace and run it locally:
16
16
 
17
17
  ```sh
18
- atlasflow init # scaffold a new agent workspace
19
- pnpm install
20
- atlasflow dev --console # watch + serve locally with an operator console
18
+ npx atlasflow init # scaffold a new agent workspace
19
+ npm install
20
+ npx atlasflow dev --console # watch + serve locally with an operator console
21
21
  ```
22
22
 
23
23
  Commands:
package/dist/index.d.ts CHANGED
@@ -12,5 +12,6 @@ declare function defineConfig(config: UserAtlasFlowConfig): UserAtlasFlowConfig;
12
12
  /** @cybernetyx1/atlasflow-cli — entrypoint and arg parsing. */
13
13
 
14
14
  declare function main(argv: string[]): Promise<void>;
15
+ declare function assertSupportedNode(version: string): void;
15
16
 
16
- export { type UserAtlasFlowConfig, defineConfig, main };
17
+ export { type UserAtlasFlowConfig, assertSupportedNode, defineConfig, main };
package/dist/index.js CHANGED
@@ -1942,7 +1942,7 @@ async function bundle(opts) {
1942
1942
  platform: "node",
1943
1943
  target: "node22",
1944
1944
  packages: "external",
1945
- sourcemap: true,
1945
+ sourcemap: false,
1946
1946
  logLevel: "silent",
1947
1947
  plugins: [skillPlugin(opts.projectRoot ?? opts.resolveDir)],
1948
1948
  ...opts.cjsPathPolyfill ? { banner: { js: 'var __filename = "/worker.mjs"; var __dirname = "/";' } } : {}
@@ -3072,7 +3072,11 @@ async function infoCommand(args) {
3072
3072
  } else {
3073
3073
  process.stdout.write(formatAgentFolderCheck(ws.agentFolder, check, { diff: includeDiff }));
3074
3074
  }
3075
- if (!check.ok) throw new Error(`atlasflow info --check failed: ${check.items.filter((item) => item.status !== "current").map((item) => `${item.name} ${item.status}`).join(", ")}`);
3075
+ if (!check.ok) {
3076
+ throw new Error(
3077
+ `atlasflow info --check failed: ${check.items.filter((item) => item.status !== "current").map((item) => `${item.name} ${item.status}`).join(", ")}. Next: run atlasflow info to regenerate .atlasflow artifacts, then rerun atlasflow info --check.`
3078
+ );
3079
+ }
3076
3080
  return;
3077
3081
  }
3078
3082
  const artifacts = writeAgentFolderArtifacts(ws.agentFolder);
@@ -3085,6 +3089,82 @@ async function infoCommand(args) {
3085
3089
  }
3086
3090
  requireAgentFolderWorkspace(ws, root);
3087
3091
  }
3092
+ async function doctorCommand(args) {
3093
+ const config = await resolveCliConfig(args);
3094
+ const root = config.root;
3095
+ loadEnv(root);
3096
+ const checks = [];
3097
+ const pkg = readProjectPackageJson(root);
3098
+ const packageManager = detectPackageManager(root, pkg.manifest);
3099
+ checks.push({
3100
+ name: "Node.js",
3101
+ ok: nodeMajor(process.versions.node) >= 22,
3102
+ detail: `current ${process.versions.node}`,
3103
+ next: "Install Node.js 22 or newer, then rerun atlasflow doctor."
3104
+ });
3105
+ checks.push({
3106
+ name: "Project package.json",
3107
+ ok: Boolean(pkg.manifest),
3108
+ detail: pkg.error ?? (pkg.manifest ? "found" : "missing"),
3109
+ next: "Run atlasflow init, or run atlasflow doctor from the project root."
3110
+ });
3111
+ checks.push({
3112
+ name: "Package manager",
3113
+ ok: Boolean(packageManager.kind),
3114
+ detail: packageManager.detail,
3115
+ next: "Run npm install to create package-lock.json, or use the package manager already chosen by this project."
3116
+ });
3117
+ const dependencyCheck = checkAtlasFlowDependencies(root, pkg.manifest);
3118
+ checks.push(dependencyCheck);
3119
+ let agent;
3120
+ try {
3121
+ const ws = loadWorkspace(root);
3122
+ agent = ws.agentFolder;
3123
+ checks.push({
3124
+ name: "Agent folder",
3125
+ ok: Boolean(agent),
3126
+ detail: agent ? `found agent/${path5.basename(agent.configFile ?? "agent.jsonc")} (${agent.name})` : "missing agent/agent.jsonc",
3127
+ next: "Run atlasflow init, or add agent/agent.jsonc plus agent/identity/profile.md."
3128
+ });
3129
+ } catch (err) {
3130
+ checks.push({
3131
+ name: "Agent folder",
3132
+ ok: false,
3133
+ detail: err instanceof Error ? err.message : String(err),
3134
+ next: "Fix agent/agent.jsonc and agent/identity/, then rerun atlasflow doctor."
3135
+ });
3136
+ }
3137
+ if (agent) {
3138
+ const modelKey = modelSecretEnvName(agent.manifest.model);
3139
+ checks.push({
3140
+ name: "Model key",
3141
+ ok: !modelKey || Boolean(process.env[modelKey]),
3142
+ detail: modelKey ? `${modelKey}${process.env[modelKey] ? " is set" : " is not set"}` : `model ${agent.manifest.model} does not require a local provider key`,
3143
+ next: modelKey ? `Set ${modelKey} in .env or the deployment secret store.` : ""
3144
+ });
3145
+ const artifactCheck = checkAgentFolderArtifacts(agent);
3146
+ checks.push({
3147
+ name: "Generated artifacts",
3148
+ ok: artifactCheck.ok,
3149
+ detail: artifactCheck.ok ? ".atlasflow artifacts are current" : artifactCheck.items.filter((item) => item.status !== "current").map((item) => `${item.name} ${item.status}`).join(", "),
3150
+ next: "Run atlasflow info to generate .atlasflow artifacts, then rerun atlasflow doctor."
3151
+ });
3152
+ }
3153
+ const failed = checks.filter((check) => !check.ok);
3154
+ if (args.json === true) {
3155
+ console.log(JSON.stringify({ ok: failed.length === 0, root, checks }, null, 2));
3156
+ if (failed.length) process.exitCode = 1;
3157
+ return;
3158
+ }
3159
+ const lines = [`AtlasFlow doctor (${root})`];
3160
+ for (const check of checks) {
3161
+ lines.push(`${check.ok ? "PASS" : "FAIL"} ${check.name}: ${check.detail}`);
3162
+ if (!check.ok) lines.push(` Next: ${check.next}`);
3163
+ }
3164
+ lines.push(failed.length ? `Status: ${failed.length} issue${failed.length === 1 ? "" : "s"} found.` : "Status: all checks passed.");
3165
+ console.log(lines.join("\n"));
3166
+ if (failed.length) process.exitCode = 1;
3167
+ }
3088
3168
  function agentFolderInfoJson(agent, artifacts, target) {
3089
3169
  return {
3090
3170
  atlasflow: 1,
@@ -3195,6 +3275,78 @@ function stableLines(value) {
3195
3275
  const trimmed = normalized.endsWith("\n") ? normalized.slice(0, -1) : normalized;
3196
3276
  return trimmed ? trimmed.split("\n") : [];
3197
3277
  }
3278
+ function nodeMajor(version) {
3279
+ const major = Number(version.split(".")[0]);
3280
+ return Number.isFinite(major) ? major : 0;
3281
+ }
3282
+ function readProjectPackageJson(root) {
3283
+ const file = path5.join(root, "package.json");
3284
+ if (!fs5.existsSync(file)) return {};
3285
+ try {
3286
+ const parsed = JSON.parse(fs5.readFileSync(file, "utf8"));
3287
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return { error: "package.json must contain an object" };
3288
+ return { manifest: parsed };
3289
+ } catch (err) {
3290
+ return { error: `invalid package.json (${err instanceof Error ? err.message : String(err)})` };
3291
+ }
3292
+ }
3293
+ function detectPackageManager(root, manifest) {
3294
+ if (fs5.existsSync(path5.join(root, "pnpm-lock.yaml"))) return { kind: "pnpm", detail: "pnpm-lock.yaml found" };
3295
+ if (fs5.existsSync(path5.join(root, "package-lock.json"))) return { kind: "npm", detail: "package-lock.json found" };
3296
+ if (fs5.existsSync(path5.join(root, "yarn.lock"))) return { kind: "yarn", detail: "yarn.lock found" };
3297
+ const packageManager = typeof manifest?.packageManager === "string" ? manifest.packageManager : void 0;
3298
+ if (packageManager?.startsWith("pnpm@")) return { kind: "pnpm", detail: `packageManager=${packageManager}; lockfile missing` };
3299
+ if (packageManager?.startsWith("npm@")) return { kind: "npm", detail: `packageManager=${packageManager}; lockfile missing` };
3300
+ if (packageManager?.startsWith("yarn@")) return { kind: "yarn", detail: `packageManager=${packageManager}; lockfile missing` };
3301
+ return { detail: "no package lockfile found" };
3302
+ }
3303
+ function checkAtlasFlowDependencies(root, manifest) {
3304
+ if (!manifest) {
3305
+ return {
3306
+ name: "AtlasFlow dependencies",
3307
+ ok: false,
3308
+ detail: "package.json missing",
3309
+ next: "Run atlasflow init."
3310
+ };
3311
+ }
3312
+ const deps = projectDependencyMap(manifest);
3313
+ const required = [atlasflowPackageName("cli"), atlasflowPackageName("runtime")];
3314
+ const missing = required.filter((name) => !deps.has(name));
3315
+ if (missing.length) {
3316
+ return {
3317
+ name: "AtlasFlow dependencies",
3318
+ ok: false,
3319
+ detail: `missing ${missing.join(", ")}`,
3320
+ next: "Run atlasflow init to merge the missing dependencies into package.json, then run npm install."
3321
+ };
3322
+ }
3323
+ const unlinked = required.filter((name) => !fs5.existsSync(path5.join(root, "node_modules", ...name.split("/"))));
3324
+ if (unlinked.length) {
3325
+ return {
3326
+ name: "AtlasFlow dependencies",
3327
+ ok: false,
3328
+ detail: `not installed ${unlinked.join(", ")}`,
3329
+ next: "Run npm install, then rerun atlasflow doctor."
3330
+ };
3331
+ }
3332
+ return {
3333
+ name: "AtlasFlow dependencies",
3334
+ ok: true,
3335
+ detail: `${required.join(", ")} installed`,
3336
+ next: ""
3337
+ };
3338
+ }
3339
+ function projectDependencyMap(manifest) {
3340
+ const out = /* @__PURE__ */ new Map();
3341
+ for (const section of ["dependencies", "devDependencies", "optionalDependencies", "peerDependencies"]) {
3342
+ const deps = manifest[section];
3343
+ if (!deps || typeof deps !== "object" || Array.isArray(deps)) continue;
3344
+ for (const [name, range] of Object.entries(deps)) {
3345
+ if (typeof range === "string") out.set(name, range);
3346
+ }
3347
+ }
3348
+ return out;
3349
+ }
3198
3350
  var AGENT_BUNDLE_ROOT = "agent";
3199
3351
  var AGENT_BUNDLE_EXCLUDED_DIRECTORIES = /* @__PURE__ */ new Set([".cache", ".git", ".turbo", ".wrangler", "dist", "node_modules"]);
3200
3352
  var AGENT_BUNDLE_EXCLUDED_FILES = /* @__PURE__ */ new Set([".ds_store"]);
@@ -3443,7 +3595,7 @@ function runProcess(command, args, cwd, label) {
3443
3595
  child.on("error", (err) => {
3444
3596
  const code = err.code;
3445
3597
  if (code === "ENOENT") {
3446
- reject(new Error(`Could not find ${command}. Run pnpm install in this workspace, install Wrangler, or pass --wrangler <path>.`));
3598
+ reject(new Error(`Could not find ${command}. Run ${suggestedInstallCommand(cwd)}, install Wrangler, or pass --wrangler <path>.`));
3447
3599
  return;
3448
3600
  }
3449
3601
  reject(err);
@@ -4725,15 +4877,17 @@ async function connectCommand(args) {
4725
4877
  var DOCS = {
4726
4878
  quickstart: `Quickstart
4727
4879
 
4728
- atlasflow init
4729
- pnpm install
4880
+ npm exec --yes --package @cybernetyx1/atlasflow-cli -- atlasflow init
4881
+ npm install
4730
4882
  cp .env.example .env
4731
4883
  # set ANTHROPIC_API_KEY in .env
4732
- atlasflow info
4733
- atlasflow run --message "hi"
4734
- atlasflow connect
4735
- atlasflow dev --console
4736
- curl -X POST localhost:4577/runs/hello -H 'content-type: application/json' -d '{"message":"hi"}'`,
4884
+ npx atlasflow info
4885
+ npx atlasflow run --message "hi"
4886
+ npx atlasflow connect
4887
+ npx atlasflow dev --console
4888
+ curl -X POST localhost:4577/runs/hello -H 'content-type: application/json' -d '{"message":"hi"}'
4889
+
4890
+ Contributors working inside the monorepo use pnpm. New app projects should not need a monorepo checkout or build.`,
4737
4891
  agents: `Authoring agents
4738
4892
 
4739
4893
  The default project has one visible agent/ folder. agent/agent.jsonc stores model
@@ -4762,6 +4916,29 @@ slot path diagnostics, and a prompt preview. Use atlasflow info --json for the
4762
4916
  machine-readable compiled graph, atlasflow info --check in CI to fail when
4763
4917
  generated artifacts are stale, and atlasflow info --check --diff to print a compact
4764
4918
  stale-artifact diff.`,
4919
+ tools: `Custom tools
4920
+
4921
+ Put custom tool modules in agent/tools/*.ts. Each module can export one default
4922
+ tool, an array of tools, { tools }, or named tool exports. Define the argument
4923
+ schema with Valibot so the runtime can validate model-provided inputs before
4924
+ execute runs.
4925
+
4926
+ agent/tools/add.ts
4927
+
4928
+ import { defineTool } from "@cybernetyx1/atlasflow-runtime";
4929
+ import * as v from "valibot";
4930
+
4931
+ export default defineTool({
4932
+ name: "add",
4933
+ description: "Add two numbers and return their sum.",
4934
+ parameters: v.object({ a: v.number(), b: v.number() }),
4935
+ execute: async ({ a, b }) => String(a + b),
4936
+ });
4937
+
4938
+ Keep side effects inside execute and keep credentials outside agent/. For external
4939
+ APIs, prefer a connection module plus an allowlist in agent/agent.jsonc so deploy-time
4940
+ secrets bind outside the agent source folder. Run atlasflow info to confirm the tool
4941
+ was discovered and atlasflow build to prove the module bundles.`,
4765
4942
  browser: `Browser integration
4766
4943
 
4767
4944
  Never put ATLASFLOW_API_KEY in browser code. Keep the master key on your server,
@@ -4863,13 +5040,14 @@ connections fail fast at run time. Never put credentials inside agent/.`,
4863
5040
  channels: `Channels
4864
5041
 
4865
5042
  Put inbound webhook/session adapters in agent/channels/<name>.ts. Channels are
4866
- for ingress: they parse platform webhooks, verify request auth, choose the stable
4867
- continuationToken, and return a dispatch for the deployed agent. Tools and
4868
- connections remain outbound capabilities the model can call. Run atlasflow add
4869
- channel <name> for a bearer-token HTTP channel, or pass --provider github|slack
4870
- for signed webhook templates. Generated templates fail closed in deployed mode
4871
- unless the required token or signing secret is configured; ATLASFLOW_MODE=local
4872
- keeps local development easy.`,
5043
+ public ingress routes by design: platform webhooks and app clients call them
5044
+ without the server master key. Every deployed channel must verify its own bearer
5045
+ token, signature, or provider secret before it dispatches to the agent, otherwise
5046
+ anyone can spend your model budget. Tools and connections remain outbound
5047
+ capabilities the model can call. Run atlasflow add channel <name> for a bearer-token
5048
+ HTTP channel, or pass --provider github|slack for signed webhook templates.
5049
+ Generated templates fail closed in deployed mode unless the required token or
5050
+ signing secret is configured; ATLASFLOW_MODE=local keeps local development easy.`,
4873
5051
  "rate-limit": `Rate limiting
4874
5052
 
4875
5053
  Built Node and Cloudflare servers enable an in-process rate limit by default for
@@ -4908,10 +5086,34 @@ Use connectMcpServer() for stdio or HTTP MCP servers. In Worker deployments, ope
4908
5086
  MCP connections lazily inside a toolSlots factory and return { tools, cleanup } so
4909
5087
  connections are created per request and closed after the run settles. Use
4910
5088
  atlasflow add mcp <name> to scaffold a lazy connector and bindings stub.`,
5089
+ prompt: `Prompt recipe
5090
+
5091
+ agent/agent.jsonc can include prompt.recipe and prompt.contextBudget. The standard
5092
+ folder scaffold uses "atlas-brain-default", which assembles identity markdown,
5093
+ principles, boundaries, skills, context, memory slots, tools, channels, and other
5094
+ folder-discovered loadout into the compiled system prompt. Use contextBudget to
5095
+ cap how much discovered context can be included eagerly. Run atlasflow info to
5096
+ inspect .atlasflow/compiled-system-prompt.md and use atlasflow info --check in CI
5097
+ to catch stale prompt artifacts.`,
4911
5098
  "structured-output": `Structured output
4912
5099
 
4913
- Declare a Valibot result schema in agent/agent.ts to validate model output. For
4914
- schema agents, data is the raw final model text and result is the parsed,
5100
+ Declare a Valibot result schema in agent/agent.ts to validate model output.
5101
+
5102
+ agent/agent.ts
5103
+
5104
+ import { defineAgent } from "@cybernetyx1/atlasflow-runtime";
5105
+ import * as v from "valibot";
5106
+
5107
+ const Result = v.object({
5108
+ status: v.picklist(["ok", "needs-info"]),
5109
+ answer: v.string(),
5110
+ });
5111
+
5112
+ export default defineAgent({
5113
+ result: Result,
5114
+ });
5115
+
5116
+ For schema agents, data is the raw final model text and result is the parsed,
4915
5117
  validated object. Use result for application logic; use data only when you want
4916
5118
  to inspect the original text. atlasflow run --json prints { ok, runId, data,
4917
5119
  result, usage } for piping into tests or automation.`,
@@ -4957,7 +5159,13 @@ Agents and workflows are plain HTTP, so any CI can call a deployed AtlasFlow:
4957
5159
  -H "authorization: Bearer $ATLASFLOW_API_KEY" \\
4958
5160
  -H "content-type: application/json" \\
4959
5161
  -d '{"payload": {"issue": 123}, "wait": true}'
4960
- Use requireApiKey() (ATLASFLOW_API_KEY) to protect the endpoints.`
5162
+ Use requireApiKey() (ATLASFLOW_API_KEY) to protect the endpoints.`,
5163
+ skill: `AtlasFlow assistant skill
5164
+
5165
+ This repository ships an assistant skill for coding agents working on AtlasFlow apps:
5166
+ npx skills add <repo>/skills/atlasflow
5167
+
5168
+ The skill tells the coding agent to read local AtlasFlow docs first, inspect the visible agent/ folder, run atlasflow info before guessing about discovery, and verify package/release work with real install and tarball evidence.`
4961
5169
  };
4962
5170
  async function docsCommand(args) {
4963
5171
  const sub = args._[0];
@@ -6511,6 +6719,8 @@ function uniqueOperationName(base, usedNames) {
6511
6719
  async function initCommand(args) {
6512
6720
  const root = rootOf(args);
6513
6721
  const force = args.force === true;
6722
+ const dryRun = args["dry-run"] === true;
6723
+ const noMerge = args["no-merge"] === true;
6514
6724
  const target = initTargetOf(str2(args, "target") ?? "node");
6515
6725
  const cloudflareRuntime = initCloudflareRuntimeOf(str2(args, "cloudflare-runtime") ?? "worker");
6516
6726
  const configPath = path5.join(root, "atlasflow.config.ts");
@@ -6521,44 +6731,175 @@ async function initCommand(args) {
6521
6731
  const identityDir = path5.join(agentDir, "identity");
6522
6732
  const agentConfigPath = path5.join(agentDir, "agent.jsonc");
6523
6733
  const identityPath = path5.join(identityDir, "profile.md");
6524
- if (fs5.existsSync(configPath) && !force) throw new Error("atlasflow.config.ts already exists (use --force to overwrite)");
6525
- if (fs5.existsSync(agentConfigPath) && !force) throw new Error("agent/agent.jsonc already exists (use --force to overwrite)");
6526
- fs5.mkdirSync(identityDir, { recursive: true });
6527
- fs5.writeFileSync(
6528
- configPath,
6529
- initConfigSource(target, cloudflareRuntime)
6530
- );
6531
- fs5.writeFileSync(
6532
- agentConfigPath,
6533
- `${JSON.stringify(
6534
- {
6535
- $schema: agentConfigSchemaRef(),
6536
- name: "hello",
6537
- model: { default: "anthropic/claude-haiku-4-5-20251001" },
6538
- skills: { loading: "lazy" },
6539
- prompt: { recipe: "atlas-brain-default" }
6540
- },
6541
- null,
6542
- 2
6543
- )}
6544
- `
6545
- );
6546
- fs5.writeFileSync(identityPath, "You are a helpful assistant. Answer concisely.\n");
6547
- const created = [configPath, agentConfigPath, identityPath];
6548
- for (const file of [
6549
- { path: packagePath, contents: initPackageJson(target) },
6550
- { path: envExamplePath, contents: INIT_ENV_EXAMPLE },
6551
- { path: gitignorePath, contents: INIT_GITIGNORE }
6552
- ]) {
6553
- if (fs5.existsSync(file.path)) continue;
6554
- fs5.writeFileSync(file.path, file.contents);
6555
- created.push(file.path);
6556
- }
6557
- console.log("Scaffolded:");
6558
- for (const file of created) console.log(` ${path5.relative(root, file)}`);
6559
- console.log("\nNext: copy .env.example to .env, set ANTHROPIC_API_KEY, then `pnpm install && pnpm dev`.");
6734
+ const writes = [];
6735
+ writes.push(planScaffoldWrite(configPath, initConfigSource(target, cloudflareRuntime), force));
6736
+ writes.push({
6737
+ ...planScaffoldWrite(
6738
+ agentConfigPath,
6739
+ `${JSON.stringify(
6740
+ {
6741
+ $schema: agentConfigSchemaRef(),
6742
+ name: "hello",
6743
+ model: { default: "anthropic/claude-haiku-4-5-20251001" },
6744
+ skills: { loading: "lazy" },
6745
+ prompt: { recipe: "atlas-brain-default" }
6746
+ },
6747
+ null,
6748
+ 2
6749
+ )}
6750
+ `,
6751
+ force
6752
+ )
6753
+ });
6754
+ writes.push(planScaffoldWrite(identityPath, "You are a helpful assistant. Answer concisely.\n", force));
6755
+ writes.push(planPackageJsonWrite(packagePath, target, noMerge));
6756
+ writes.push(planTextBlockWrite(envExamplePath, INIT_ENV_EXAMPLE, "AtlasFlow", initEnvExampleLines(), initEnvKey, noMerge));
6757
+ writes.push(planTextBlockWrite(gitignorePath, INIT_GITIGNORE, "AtlasFlow", INIT_GITIGNORE.split("\n").filter(Boolean), void 0, noMerge));
6758
+ if (!dryRun) {
6759
+ fs5.mkdirSync(identityDir, { recursive: true });
6760
+ for (const write of writes) {
6761
+ if (write.action === "skip") continue;
6762
+ fs5.mkdirSync(path5.dirname(write.file), { recursive: true });
6763
+ fs5.writeFileSync(write.file, write.contents);
6764
+ }
6765
+ }
6766
+ console.log(dryRun ? "Dry run: planned scaffold changes:" : "Scaffolded:");
6767
+ for (const write of writes) {
6768
+ const suffix = write.note ? ` - ${write.note}` : "";
6769
+ console.log(` ${write.action.padEnd(9)} ${path5.relative(root, write.file)}${suffix}`);
6770
+ }
6771
+ const install = suggestedInstallCommand(root);
6772
+ console.log(`
6773
+ Next: copy .env.example to .env, set ANTHROPIC_API_KEY, then \`${install} && atlasflow dev\`.`);
6560
6774
  console.log("Inspect: atlasflow info");
6561
6775
  }
6776
+ function planScaffoldWrite(file, contents, force) {
6777
+ if (!fs5.existsSync(file)) return { file, action: "create", contents };
6778
+ if (force) return { file, action: "overwrite", contents };
6779
+ return { file, action: "skip", contents: fs5.readFileSync(file, "utf8"), note: "exists; use --force to overwrite" };
6780
+ }
6781
+ function planPackageJsonWrite(file, target, noMerge) {
6782
+ const generated = initPackageJsonObject(target);
6783
+ if (!fs5.existsSync(file)) return { file, action: "create", contents: `${JSON.stringify(generated, null, 2)}
6784
+ ` };
6785
+ if (noMerge) return { file, action: "skip", contents: fs5.readFileSync(file, "utf8"), note: "--no-merge" };
6786
+ const raw = fs5.readFileSync(file, "utf8");
6787
+ const current = parseJsonObject(raw, file);
6788
+ const { manifest, changed, note } = mergeInitPackageJson(current, generated);
6789
+ return { file, action: changed ? "merge" : "skip", contents: `${JSON.stringify(manifest, null, 2)}
6790
+ `, note };
6791
+ }
6792
+ function mergeInitPackageJson(current, generated) {
6793
+ const manifest = { ...current };
6794
+ let changed = false;
6795
+ const preserved = [];
6796
+ for (const [key, value] of Object.entries(generated)) {
6797
+ if (key === "scripts" || key === "dependencies" || key === "devDependencies") continue;
6798
+ if (manifest[key] === void 0) {
6799
+ manifest[key] = value;
6800
+ changed = true;
6801
+ }
6802
+ }
6803
+ const scripts = ensurePlainObject(manifest, "scripts");
6804
+ const generatedScripts = objectEntriesOfStrings(generated.scripts);
6805
+ for (const [name, command] of generatedScripts) {
6806
+ const existing = scripts[name];
6807
+ if (existing === void 0) {
6808
+ scripts[name] = command;
6809
+ changed = true;
6810
+ } else if (existing !== command) {
6811
+ const atlasName = `atlasflow:${name}`;
6812
+ if (scripts[atlasName] === void 0) {
6813
+ scripts[atlasName] = command;
6814
+ changed = true;
6815
+ } else if (scripts[atlasName] !== command) {
6816
+ preserved.push(`scripts.${atlasName}`);
6817
+ }
6818
+ }
6819
+ }
6820
+ const existingDeps = dependencyLocations(manifest);
6821
+ for (const section of ["dependencies", "devDependencies"]) {
6822
+ const target = ensurePlainObject(manifest, section);
6823
+ for (const [name, range] of objectEntriesOfStrings(generated[section])) {
6824
+ if (existingDeps.has(name)) {
6825
+ preserved.push(`${existingDeps.get(name).section}.${name}`);
6826
+ continue;
6827
+ }
6828
+ target[name] = range;
6829
+ existingDeps.set(name, { section, range });
6830
+ changed = true;
6831
+ }
6832
+ }
6833
+ return {
6834
+ manifest,
6835
+ changed,
6836
+ note: preserved.length ? `preserved existing ${[...new Set(preserved)].join(", ")}` : void 0
6837
+ };
6838
+ }
6839
+ function ensurePlainObject(parent, key) {
6840
+ const current = parent[key];
6841
+ if (current && typeof current === "object" && !Array.isArray(current)) return current;
6842
+ const next = {};
6843
+ parent[key] = next;
6844
+ return next;
6845
+ }
6846
+ function objectEntriesOfStrings(value) {
6847
+ if (!value || typeof value !== "object" || Array.isArray(value)) return [];
6848
+ return Object.entries(value).filter((entry) => typeof entry[1] === "string");
6849
+ }
6850
+ function dependencyLocations(manifest) {
6851
+ const out = /* @__PURE__ */ new Map();
6852
+ for (const section of ["dependencies", "devDependencies", "optionalDependencies", "peerDependencies"]) {
6853
+ for (const [name, range] of objectEntriesOfStrings(manifest[section])) out.set(name, { section, range });
6854
+ }
6855
+ return out;
6856
+ }
6857
+ function planTextBlockWrite(file, createContents, label, desiredLines, keyOf, noMerge = false) {
6858
+ if (!fs5.existsSync(file)) return { file, action: "create", contents: createContents };
6859
+ const raw = fs5.readFileSync(file, "utf8");
6860
+ if (noMerge) return { file, action: "skip", contents: raw, note: "--no-merge" };
6861
+ const merged = mergeTextBlock(raw, label, desiredLines, keyOf);
6862
+ return {
6863
+ file,
6864
+ action: merged.changed ? "merge" : "skip",
6865
+ contents: merged.contents,
6866
+ note: merged.added.length ? `added ${merged.added.join(", ")}` : void 0
6867
+ };
6868
+ }
6869
+ function mergeTextBlock(raw, label, desiredLines, keyOf) {
6870
+ const normalized = raw.replace(/\r\n/g, "\n");
6871
+ const existingLines = new Set(normalized.split("\n").map((line) => line.trim()));
6872
+ const existingKeys = new Set(normalized.split("\n").map((line) => keyOf?.(line.trim())).filter((key) => Boolean(key)));
6873
+ const missing = desiredLines.filter((line) => {
6874
+ const key = keyOf?.(line);
6875
+ if (key) return !existingKeys.has(key);
6876
+ return line.trim() && !existingLines.has(line.trim());
6877
+ });
6878
+ if (!missing.length) return { contents: raw, changed: false, added: [] };
6879
+ const prefix = normalized.endsWith("\n") || normalized.length === 0 ? "" : "\n";
6880
+ const block = [`# ${label}`, ...missing, ""].join("\n");
6881
+ return { contents: `${normalized}${prefix}${block}`, changed: true, added: missing.map((line) => keyOf?.(line) ?? line) };
6882
+ }
6883
+ function initEnvExampleLines() {
6884
+ return INIT_ENV_EXAMPLE.split("\n").filter((line) => {
6885
+ const trimmed = line.trim();
6886
+ return trimmed && trimmed !== "# Copy this file to .env for local development.";
6887
+ });
6888
+ }
6889
+ function initEnvKey(line) {
6890
+ const match = line.match(/^\s*#?\s*([A-Z][A-Z0-9_]+)=/);
6891
+ return match?.[1];
6892
+ }
6893
+ function parseJsonObject(raw, file) {
6894
+ const parsed = JSON.parse(raw);
6895
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) throw new Error(`Invalid JSON object: ${file}`);
6896
+ return parsed;
6897
+ }
6898
+ function suggestedInstallCommand(root) {
6899
+ if (fs5.existsSync(path5.join(root, "pnpm-lock.yaml"))) return "pnpm install";
6900
+ if (fs5.existsSync(path5.join(root, "yarn.lock"))) return "yarn install";
6901
+ return "npm install";
6902
+ }
6562
6903
  function initTargetOf(value) {
6563
6904
  if (value === "node" || value === "cloudflare") return value;
6564
6905
  throw new Error('atlasflow init target must be "node" or "cloudflare"');
@@ -6578,9 +6919,9 @@ function initConfigSource(target, cloudflareRuntime) {
6578
6919
  lines.push("});", "");
6579
6920
  return lines.join("\n");
6580
6921
  }
6581
- function initPackageJson(target) {
6922
+ function initPackageJsonObject(target) {
6582
6923
  const version = atlasflowPackageVersion();
6583
- const json = {
6924
+ return {
6584
6925
  name: "atlasflow-app",
6585
6926
  private: true,
6586
6927
  type: "module",
@@ -6600,8 +6941,6 @@ function initPackageJson(target) {
6600
6941
  ...target === "cloudflare" ? { wrangler: "^4.87.0" } : {}
6601
6942
  }
6602
6943
  };
6603
- return `${JSON.stringify(json, null, 2)}
6604
- `;
6605
6944
  }
6606
6945
  var INIT_ENV_EXAMPLE = [
6607
6946
  "# Copy this file to .env for local development.",
@@ -6645,6 +6984,7 @@ Usage:
6645
6984
  atlasflow dev [--port <n>] [--console] watch + serve locally; optional operator console
6646
6985
  atlasflow build [--output <dir>] bundle a deployable Node server
6647
6986
  atlasflow deploy [--dry-run] build and deploy one Worker via Wrangler
6987
+ atlasflow doctor diagnose setup and print the next fix
6648
6988
  atlasflow info [--json|--check] inspect or verify the discovered agent graph
6649
6989
  atlasflow run [agent] [--message <t>] invoke an agent once (CI-style)
6650
6990
  [--payload <json>]
@@ -6664,6 +7004,7 @@ Usage:
6664
7004
  Common flags: --root <dir>, --target node|cloudflare, --cloudflare-runtime worker|agents
6665
7005
  `;
6666
7006
  async function main(argv) {
7007
+ assertSupportedNode(process.versions.node);
6667
7008
  const [command, ...rest] = argv;
6668
7009
  const args = parseArgs(rest);
6669
7010
  switch (command) {
@@ -6675,6 +7016,8 @@ async function main(argv) {
6675
7016
  return buildCommand(args);
6676
7017
  case "deploy":
6677
7018
  return deployCommand(args);
7019
+ case "doctor":
7020
+ return doctorCommand(args);
6678
7021
  case "info":
6679
7022
  return infoCommand(args);
6680
7023
  case "run":
@@ -6700,10 +7043,17 @@ async function main(argv) {
6700
7043
  process.stdout.write(HELP);
6701
7044
  return;
6702
7045
  default:
6703
- throw new Error(`Unknown command "${command}". Try: init, dev, build, deploy, info, run, connect, eval, logs, add, export, import, docs`);
7046
+ throw new Error(`Unknown command "${command}". Try: init, dev, build, deploy, doctor, info, run, connect, eval, logs, add, export, import, docs`);
7047
+ }
7048
+ }
7049
+ function assertSupportedNode(version) {
7050
+ const major = Number(version.split(".")[0]);
7051
+ if (!Number.isFinite(major) || major < 22) {
7052
+ throw new Error(`AtlasFlow requires Node.js 22 or newer. Current Node.js: ${version}. Next: install Node 22+ and rerun the command.`);
6704
7053
  }
6705
7054
  }
6706
7055
  export {
7056
+ assertSupportedNode,
6707
7057
  defineConfig,
6708
7058
  main
6709
7059
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cybernetyx1/atlasflow-cli",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "description": "Command-line tool for authoring, inspecting, building, and deploying AtlasFlow agents.",
5
5
  "type": "module",
6
6
  "license": "SEE LICENSE IN LICENSE",
@@ -27,7 +27,7 @@
27
27
  "dependencies": {
28
28
  "esbuild": "^0.24.0",
29
29
  "yaml": "^2.9.0",
30
- "@cybernetyx1/atlasflow-runtime": "0.1.0"
30
+ "@cybernetyx1/atlasflow-runtime": "0.1.2"
31
31
  },
32
32
  "devDependencies": {
33
33
  "@types/node": "^22.10.0",