@deepraxis/cli 0.1.1-dev.f0ef747 → 0.2.0-dev.3c36146
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/COMMANDS.md +425 -423
- package/README.md +39 -24
- package/dist/praxis.js +732 -60
- package/package.json +1 -1
package/dist/praxis.js
CHANGED
|
@@ -1973,7 +1973,7 @@ clears to null; an empty string is stored as an empty string.`
|
|
|
1973
1973
|
resource: "agent.sessions",
|
|
1974
1974
|
method: "create",
|
|
1975
1975
|
summary: "Create session",
|
|
1976
|
-
description: `Create a session: one conversation with an agent. Pass agent as an agent id or {type:"agent", id, version?}; an id alone, or an object without version, pins the agent's active_version (the version chosen with agents.versions.apply, not necessarily the newest saved one). Unlike Anthropic Managed Agents, omit environment_id to use the default environment saved in the agent version (light for a new agent), unless computer_id (explicit, or the agent's default) selects a Computer; pass an environment id for a sandbox recipe. For service agents, explicit null allocates no machine, so codemode, bash and other machine tools are unavailable. Optional model must be one of the agent's allowed models; optional thinking is {effort: low|medium|high|xhigh|max|none} or null, where null inherits the agent default and then the model default (none requires the model to allow disabling thinking). Selections are saved before initial_events run; invalid ones return 400 invalid_request_error. Unlike Anthropic Managed Agents, the only inline agent override is system ({type:"agent_with_overrides", id, system}); to change the model, tools, MCP servers or skills, save a version instead. Supports vault_ids, up to 50, owned by the current organization and not archived; credential injection is deferred to a later version. budget is {type:"limit", max_list_cost:{amount, currency:"USD"}} where amount is a positive
|
|
1976
|
+
description: `Create a session: one conversation with an agent. Pass agent as an agent id or {type:"agent", id, version?}; an id alone, or an object without version, pins the agent's active_version (the version chosen with agents.versions.apply, not necessarily the newest saved one). Unlike Anthropic Managed Agents, omit environment_id to use the default environment saved in the agent version (light for a new agent), unless computer_id (explicit, or the agent's default) selects a Computer; pass an environment id for a sandbox recipe. For service agents, explicit null allocates no machine, so codemode, bash and other machine tools are unavailable. Optional model must be one of the agent's allowed models; optional thinking is {effort: low|medium|high|xhigh|max|none} or null, where null inherits the agent default and then the model default (none requires the model to allow disabling thinking). Selections are saved before initial_events run; invalid ones return 400 invalid_request_error. Unlike Anthropic Managed Agents, the only inline agent override is system ({type:"agent_with_overrides", id, system}); to change the model, tools, MCP servers or skills, save a version instead. Supports vault_ids, up to 50, owned by the current organization and not archived; credential injection is deferred to a later version. budget is {type:"limit", max_list_cost:{amount, currency:"USD"}} where amount is a positive decimal string of US dollars with at most 2 decimal places ("20.00" is USD 20.00); omitted budget defaults to "20.00" and null removes the limit. Up to 500 file resources can be attached. Use initial_events to send a first user.message in this request, or send messages afterwards with sessions.events.send. The returned status can still be idle before initial events run; read agent.message and the subsequent session.status_idle event to confirm completion.`,
|
|
1977
1977
|
http: "POST",
|
|
1978
1978
|
path: "/agent/v1/sessions",
|
|
1979
1979
|
streaming: "no",
|
|
@@ -2549,9 +2549,9 @@ Available only on /agent/v1. Use the message_id field from own-face events, not
|
|
|
2549
2549
|
query: [
|
|
2550
2550
|
{
|
|
2551
2551
|
name: "limit",
|
|
2552
|
-
type: "
|
|
2552
|
+
type: "integer",
|
|
2553
2553
|
required: false,
|
|
2554
|
-
description: "Number of
|
|
2554
|
+
description: "Number of resources per page, from 1 to 100; defaults to 100."
|
|
2555
2555
|
},
|
|
2556
2556
|
{
|
|
2557
2557
|
name: "page",
|
|
@@ -9610,7 +9610,8 @@ The default is return=minimal: success returns HTTP 201 with an empty body. Send
|
|
|
9610
9610
|
{
|
|
9611
9611
|
name: "file_id",
|
|
9612
9612
|
type: "string",
|
|
9613
|
-
required: true
|
|
9613
|
+
required: true,
|
|
9614
|
+
description: "File ID to copy into this library; passed as the upload source instead of file bytes."
|
|
9614
9615
|
},
|
|
9615
9616
|
{
|
|
9616
9617
|
name: "public_id",
|
|
@@ -25061,7 +25062,7 @@ async function publish(client2, input) {
|
|
|
25061
25062
|
const timeout = input.timeout_ms ?? 120000;
|
|
25062
25063
|
if (!Number.isFinite(timeout) || timeout < 0)
|
|
25063
25064
|
throw new RangeError("timeout_ms must be non-negative and finite");
|
|
25064
|
-
const
|
|
25065
|
+
const moduleTypes = [
|
|
25065
25066
|
"application/javascript+module",
|
|
25066
25067
|
"text/javascript",
|
|
25067
25068
|
"application/javascript",
|
|
@@ -25070,9 +25071,30 @@ async function publish(client2, input) {
|
|
|
25070
25071
|
"application/wasm",
|
|
25071
25072
|
"application/octet-stream",
|
|
25072
25073
|
"application/x-python"
|
|
25073
|
-
]
|
|
25074
|
-
|
|
25075
|
-
|
|
25074
|
+
];
|
|
25075
|
+
const moduleType = (type) => {
|
|
25076
|
+
const found = moduleTypes.find((candidate) => candidate === (type ?? "application/javascript+module"));
|
|
25077
|
+
if (!found)
|
|
25078
|
+
throw new PraxisError({ code: "INVALID_PARAMS", message: `Unsupported module content_type: ${type}` });
|
|
25079
|
+
return found;
|
|
25080
|
+
};
|
|
25081
|
+
if (input.main && input.modules)
|
|
25082
|
+
throw new PraxisError({ code: "INVALID_PARAMS", message: "Pass either main or modules, not both" });
|
|
25083
|
+
if (input.main_module !== undefined && !input.modules)
|
|
25084
|
+
throw new PraxisError({ code: "INVALID_PARAMS", message: "main_module requires modules; with main the entry is main.name" });
|
|
25085
|
+
if (input.modules && input.modules.length === 0)
|
|
25086
|
+
throw new PraxisError({ code: "INVALID_PARAMS", message: "modules must not be empty" });
|
|
25087
|
+
const entryModules = input.main ? [{ name: input.main.name, content_type: moduleType(input.main.content_type), content_base64: base64(new TextEncoder().encode(input.main.content)) }] : input.modules?.map((module) => ({
|
|
25088
|
+
name: module.name,
|
|
25089
|
+
content_type: moduleType(module.content_type),
|
|
25090
|
+
content_base64: base64(typeof module.content === "string" ? new TextEncoder().encode(module.content) : module.content)
|
|
25091
|
+
}));
|
|
25092
|
+
if (new Set(entryModules?.map((module) => module.name)).size !== (entryModules?.length ?? 0))
|
|
25093
|
+
throw new PraxisError({ code: "INVALID_PARAMS", message: "Duplicate module name" });
|
|
25094
|
+
const mainModule = input.main ? input.main.name : input.modules ? input.main_module ?? input.modules[0].name : undefined;
|
|
25095
|
+
if (input.modules && !input.modules.some((module) => module.name === mainModule)) {
|
|
25096
|
+
throw new PraxisError({ code: "INVALID_PARAMS", message: `main_module ${mainModule} is not among modules` });
|
|
25097
|
+
}
|
|
25076
25098
|
const files = Object.entries(input.files);
|
|
25077
25099
|
if (files.length > 1e5)
|
|
25078
25100
|
throw new PraxisError({ code: "LIMIT_EXCEEDED", message: "At most 100000 asset paths can be published" });
|
|
@@ -25125,7 +25147,7 @@ async function publish(client2, input) {
|
|
|
25125
25147
|
const removed = Object.keys(previous).filter((key) => !Object.hasOwn(manifest, key));
|
|
25126
25148
|
const digest = new Uint8Array(await crypto.subtle.digest("SHA-256", new TextEncoder().encode(JSON.stringify(manifest))));
|
|
25127
25149
|
const manifestHash = Array.from(digest, (byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
25128
|
-
const staticOnly = !["main", "bindings", "compatibility_date", "compatibility_flags", "assets_config"].some((key) => Object.hasOwn(input, key));
|
|
25150
|
+
const staticOnly = !["main", "modules", "main_module", "bindings", "compatibility_date", "compatibility_flags", "assets_config"].some((key) => Object.hasOwn(input, key));
|
|
25129
25151
|
const unchanged = staticOnly && !!currentVersion && manifestHash === currentVersion.praxis.assets_manifest?.manifest_hash;
|
|
25130
25152
|
const hashes = new Set(Object.values(manifest).map((entry) => entry.hash));
|
|
25131
25153
|
const currentResult = {
|
|
@@ -25181,11 +25203,7 @@ async function publish(client2, input) {
|
|
|
25181
25203
|
assets: versionAssets,
|
|
25182
25204
|
...input.compatibility_flags ? { compatibility_flags: input.compatibility_flags } : {},
|
|
25183
25205
|
...input.bindings ? { bindings: input.bindings } : {},
|
|
25184
|
-
...
|
|
25185
|
-
name: input.main.name,
|
|
25186
|
-
content_type: contentType,
|
|
25187
|
-
content_base64: base64(new TextEncoder().encode(input.main.content))
|
|
25188
|
-
}] } : {}
|
|
25206
|
+
...entryModules ? { main_module: mainModule, modules: entryModules } : {}
|
|
25189
25207
|
} });
|
|
25190
25208
|
let deployment = result.deployment;
|
|
25191
25209
|
if (!deployment)
|
|
@@ -25316,7 +25334,7 @@ function decideTabReuse(targetUrl, tabs) {
|
|
|
25316
25334
|
if (best === undefined || rank < best.rank || rank === best.rank && (tab.active || !best.tab.active))
|
|
25317
25335
|
best = { tab, rank };
|
|
25318
25336
|
}
|
|
25319
|
-
return best?.tab;
|
|
25337
|
+
return best?.tab ?? tabs.find((tab) => isBlankTab(tab.url));
|
|
25320
25338
|
}
|
|
25321
25339
|
function buildConnectUrl(parts) {
|
|
25322
25340
|
const url = new URL("/browser/v1/connect", parts.api);
|
|
@@ -25334,6 +25352,16 @@ function pickAgentContext(contexts, agentId) {
|
|
|
25334
25352
|
return [...own].sort((x, y) => (x.createdAt ?? "").localeCompare(y.createdAt ?? "") || x.id.localeCompare(y.id))[0];
|
|
25335
25353
|
}
|
|
25336
25354
|
var agentContextName = (agentId) => `agent-${agentId}`.replace(/[^A-Za-z0-9._:-]/g, "-").slice(0, 128);
|
|
25355
|
+
function isBlankTab(value) {
|
|
25356
|
+
if (value === "" || value === "about:blank")
|
|
25357
|
+
return true;
|
|
25358
|
+
try {
|
|
25359
|
+
const url = new URL(value);
|
|
25360
|
+
return (url.protocol === "https:" || url.protocol === "http:") && url.pathname === "/start" && (url.hostname.startsWith("console-") || url.hostname === "console.deepraxis.com");
|
|
25361
|
+
} catch {
|
|
25362
|
+
return false;
|
|
25363
|
+
}
|
|
25364
|
+
}
|
|
25337
25365
|
|
|
25338
25366
|
// ../../packages/sdk/typescript/src/helpers/browser-connect.ts
|
|
25339
25367
|
var PLAYWRIGHT_CORE_VERSION = "1.63.0";
|
|
@@ -25532,6 +25560,116 @@ class Praxis {
|
|
|
25532
25560
|
}
|
|
25533
25561
|
}
|
|
25534
25562
|
}
|
|
25563
|
+
// ../../packages/sdk/typescript/src/helpers/application-build.ts
|
|
25564
|
+
var UNSUPPORTED_BINDING_KEYS = [
|
|
25565
|
+
"services",
|
|
25566
|
+
"queues",
|
|
25567
|
+
"durable_objects",
|
|
25568
|
+
"ai",
|
|
25569
|
+
"analytics_engine_datasets",
|
|
25570
|
+
"vectorize",
|
|
25571
|
+
"hyperdrive",
|
|
25572
|
+
"browser",
|
|
25573
|
+
"send_email",
|
|
25574
|
+
"mtls_certificates",
|
|
25575
|
+
"dispatch_namespaces",
|
|
25576
|
+
"unsafe",
|
|
25577
|
+
"version_metadata",
|
|
25578
|
+
"images",
|
|
25579
|
+
"workflows",
|
|
25580
|
+
"pipelines",
|
|
25581
|
+
"secrets_store_secrets",
|
|
25582
|
+
"ratelimits",
|
|
25583
|
+
"vpc_services",
|
|
25584
|
+
"media",
|
|
25585
|
+
"tail_consumers",
|
|
25586
|
+
"worker_loaders"
|
|
25587
|
+
];
|
|
25588
|
+
var isPresent = (value) => {
|
|
25589
|
+
if (value === undefined || value === null)
|
|
25590
|
+
return false;
|
|
25591
|
+
if (Array.isArray(value))
|
|
25592
|
+
return value.some(isPresent);
|
|
25593
|
+
if (typeof value === "object")
|
|
25594
|
+
return Object.values(value).some(isPresent);
|
|
25595
|
+
return true;
|
|
25596
|
+
};
|
|
25597
|
+
function ignoreMatcher(line) {
|
|
25598
|
+
let pattern = line.trim();
|
|
25599
|
+
if (!pattern || pattern.startsWith("#") || pattern.startsWith("!"))
|
|
25600
|
+
return null;
|
|
25601
|
+
const anchored = pattern.startsWith("/") || pattern.slice(0, -1).includes("/");
|
|
25602
|
+
pattern = pattern.replace(/^\//, "").replace(/\/$/, "");
|
|
25603
|
+
const source = pattern.split("**").map((part) => part.split("*").map((piece) => piece.replace(/[.+?^${}()|[\]\\]/g, "\\$&")).join("[^/]*")).join(".*");
|
|
25604
|
+
const regex = new RegExp(`^${source}$`);
|
|
25605
|
+
return (path) => {
|
|
25606
|
+
const segments = path.split("/");
|
|
25607
|
+
for (let end = 1;end <= segments.length; end++) {
|
|
25608
|
+
if (anchored ? regex.test(segments.slice(0, end).join("/")) : regex.test(segments[end - 1]))
|
|
25609
|
+
return true;
|
|
25610
|
+
}
|
|
25611
|
+
return false;
|
|
25612
|
+
};
|
|
25613
|
+
}
|
|
25614
|
+
function bindingsFrom(wrangler) {
|
|
25615
|
+
const bindings = [];
|
|
25616
|
+
const fromList = (key, type) => {
|
|
25617
|
+
const list = wrangler[key];
|
|
25618
|
+
if (!isPresent(list))
|
|
25619
|
+
return;
|
|
25620
|
+
if (!Array.isArray(list))
|
|
25621
|
+
throw new PraxisError({ code: "INVALID_PARAMS", message: `wrangler.json ${key} must be an array` });
|
|
25622
|
+
for (const entry of list) {
|
|
25623
|
+
if (typeof entry?.binding !== "string")
|
|
25624
|
+
throw new PraxisError({ code: "INVALID_PARAMS", message: `wrangler.json ${key} entry has no binding name` });
|
|
25625
|
+
bindings.push({ type, name: entry.binding });
|
|
25626
|
+
}
|
|
25627
|
+
};
|
|
25628
|
+
fromList("d1_databases", "d1");
|
|
25629
|
+
fromList("kv_namespaces", "kv_namespace");
|
|
25630
|
+
fromList("r2_buckets", "r2_bucket");
|
|
25631
|
+
for (const [name, value] of Object.entries(wrangler.vars ?? {})) {
|
|
25632
|
+
bindings.push(typeof value === "string" ? { type: "plain_text", name, text: value } : { type: "json", name, json: value });
|
|
25633
|
+
}
|
|
25634
|
+
if (typeof wrangler.assets?.binding === "string")
|
|
25635
|
+
bindings.push({ type: "assets", name: wrangler.assets.binding });
|
|
25636
|
+
return bindings;
|
|
25637
|
+
}
|
|
25638
|
+
function publishInputFromBuild(build, name) {
|
|
25639
|
+
const wrangler = build.wranglerJson;
|
|
25640
|
+
if (!wrangler || typeof wrangler !== "object" || Array.isArray(wrangler)) {
|
|
25641
|
+
throw new PraxisError({ code: "INVALID_PARAMS", message: "wranglerJson must be the parsed dist/server/wrangler.json object" });
|
|
25642
|
+
}
|
|
25643
|
+
const offending = UNSUPPORTED_BINDING_KEYS.filter((key) => isPresent(wrangler[key]));
|
|
25644
|
+
if (offending.length) {
|
|
25645
|
+
throw new PraxisError({ code: "UNSUPPORTED", message: `wrangler.json declares bindings the platform does not support: ${offending.join(", ")}` });
|
|
25646
|
+
}
|
|
25647
|
+
const modules = Object.entries(build.serverFiles).filter(([path]) => path.endsWith(".js")).map(([path, content]) => ({ name: path, content, content_type: "application/javascript+module" }));
|
|
25648
|
+
if (typeof wrangler.main !== "string")
|
|
25649
|
+
throw new PraxisError({ code: "INVALID_PARAMS", message: "wrangler.json has no main entry" });
|
|
25650
|
+
if (!modules.some((module) => module.name === wrangler.main)) {
|
|
25651
|
+
throw new PraxisError({ code: "INVALID_PARAMS", message: `wrangler.json main ${wrangler.main} is not a .js file under dist/server` });
|
|
25652
|
+
}
|
|
25653
|
+
const ignored = new TextDecoder().decode(build.clientFiles[".assetsignore"] ?? new Uint8Array).split(/\r?\n/).map(ignoreMatcher).filter((matcher) => matcher !== null);
|
|
25654
|
+
const files = {};
|
|
25655
|
+
for (const [path, content] of Object.entries(build.clientFiles)) {
|
|
25656
|
+
if (path === ".assetsignore" || ignored.some((matches) => matches(path)))
|
|
25657
|
+
continue;
|
|
25658
|
+
files[`/${path}`] = content;
|
|
25659
|
+
}
|
|
25660
|
+
const { binding: _binding, directory: _directory, ...assetsConfig } = wrangler.assets ?? {};
|
|
25661
|
+
const bindings = bindingsFrom(wrangler);
|
|
25662
|
+
return {
|
|
25663
|
+
name,
|
|
25664
|
+
files,
|
|
25665
|
+
modules,
|
|
25666
|
+
main_module: wrangler.main,
|
|
25667
|
+
...typeof wrangler.compatibility_date === "string" ? { compatibility_date: wrangler.compatibility_date } : {},
|
|
25668
|
+
...Array.isArray(wrangler.compatibility_flags) ? { compatibility_flags: wrangler.compatibility_flags } : {},
|
|
25669
|
+
...bindings.length ? { bindings } : {},
|
|
25670
|
+
...Object.keys(assetsConfig).length ? { assets_config: assetsConfig } : {}
|
|
25671
|
+
};
|
|
25672
|
+
}
|
|
25535
25673
|
// src/core/command-overrides.ts
|
|
25536
25674
|
var commandOverrides = {
|
|
25537
25675
|
"agent.deployments.run.create": { words: ["agent", "deployment", "run"], reason: "The terminal path is an action; expose its verb without a redundant create." },
|
|
@@ -25592,6 +25730,15 @@ function commandWords(resource, method) {
|
|
|
25592
25730
|
const verb = kebab(method.replace(/^retrieve(?=[A-Z]|$)/, "get"));
|
|
25593
25731
|
return [domain, ...resources, verb];
|
|
25594
25732
|
}
|
|
25733
|
+
var API_WORD = "api";
|
|
25734
|
+
var HTTP_METHOD = /^(GET|POST|PUT|PATCH|DELETE|HEAD|OPTIONS)$/;
|
|
25735
|
+
var domainOf = (command) => command.words[1];
|
|
25736
|
+
function movedTo(commands, words) {
|
|
25737
|
+
if (!words.length || words[0] === API_WORD)
|
|
25738
|
+
return;
|
|
25739
|
+
const moved = [API_WORD, ...words];
|
|
25740
|
+
return commands.some((c) => moved.every((w, i) => c.words[i] === w)) ? `praxis ${moved.join(" ")}` : undefined;
|
|
25741
|
+
}
|
|
25595
25742
|
function commandTree(operations2) {
|
|
25596
25743
|
const commands = [];
|
|
25597
25744
|
const seen = new Set;
|
|
@@ -25607,7 +25754,7 @@ function commandTree(operations2) {
|
|
|
25607
25754
|
if (["login", "logout", "whoami", "org", "api", "skill"].includes(words[0]))
|
|
25608
25755
|
throw new Error(`Reserved command: ${key}`);
|
|
25609
25756
|
seen.add(key);
|
|
25610
|
-
commands.push({ words, op, ...method !== op.method ? { alias: method } : {} });
|
|
25757
|
+
commands.push({ words: [API_WORD, ...words], op, ...method !== op.method ? { alias: method } : {} });
|
|
25611
25758
|
}
|
|
25612
25759
|
}
|
|
25613
25760
|
for (const key of Object.keys(commandOverrides))
|
|
@@ -25903,29 +26050,112 @@ function help(commands, prefix, command) {
|
|
|
25903
26050
|
`);
|
|
25904
26051
|
}
|
|
25905
26052
|
const matching = commands.filter((c) => prefix.every((w, i) => c.words[i] === w));
|
|
26053
|
+
const global = [
|
|
26054
|
+
"Global options:",
|
|
26055
|
+
...globalOptions.map((s) => ` ${describeOption(s)}`),
|
|
26056
|
+
" -h is an alias of --help",
|
|
26057
|
+
"PRAXIS_ENV=develop|production (default: develop); no --env option.",
|
|
26058
|
+
""
|
|
26059
|
+
];
|
|
26060
|
+
const sample = matching.find((c) => c.words.at(-1) === "list") ?? matching[0];
|
|
26061
|
+
if (!prefix.length) {
|
|
26062
|
+
const nouns = [...new Set(workflows.map((w) => w.words[0]))];
|
|
26063
|
+
const summaries = new Map(workflows.map((w) => [w.words[0], w.words.length === 1 ? w.summary : undefined]));
|
|
26064
|
+
const width = Math.max(...nouns.map((n) => n.length));
|
|
26065
|
+
return [
|
|
26066
|
+
"praxis <command> [options]",
|
|
26067
|
+
"",
|
|
26068
|
+
"Workflows:",
|
|
26069
|
+
...nouns.map((n) => ` ${n.padEnd(width)} ${summaries.get(n) ?? workflows.filter((w) => w.words[0] === n).map((w) => w.words[1]).join(", ")}`),
|
|
26070
|
+
"",
|
|
26071
|
+
"API:",
|
|
26072
|
+
` praxis ${API_WORD} <domain> <resource> <action> [options] one command per API operation`,
|
|
26073
|
+
` praxis ${API_WORD} <METHOD> <path> [--data JSON|@file|-] raw request`,
|
|
26074
|
+
` Domains: ${domains(commands).join(", ")}`,
|
|
26075
|
+
"",
|
|
26076
|
+
...global,
|
|
26077
|
+
"Examples:",
|
|
26078
|
+
` ${sample ? example(sample) : "praxis --help"}`
|
|
26079
|
+
].join(`
|
|
26080
|
+
`);
|
|
26081
|
+
}
|
|
25906
26082
|
const children = [...new Set(matching.map((c) => c.words[prefix.length]).filter(Boolean))].sort();
|
|
25907
|
-
|
|
25908
|
-
children.unshift("login", "logout", "whoami", "org", "api", "skill");
|
|
26083
|
+
const raw = prefix.length === 1 && prefix[0] === API_WORD;
|
|
25909
26084
|
return [
|
|
25910
26085
|
`praxis${prefix.length ? " " + prefix.join(" ") : ""} <command> [options]`,
|
|
25911
26086
|
"",
|
|
26087
|
+
...raw ? ["Domains:"] : [],
|
|
25912
26088
|
...children.map((s) => ` ${s}`),
|
|
25913
26089
|
"",
|
|
25914
|
-
"
|
|
25915
|
-
...
|
|
26090
|
+
...raw ? ["Raw request:", ` praxis ${API_WORD} <METHOD> <path> [--data JSON|@file|-]`, ""] : [],
|
|
26091
|
+
...global,
|
|
26092
|
+
"Examples:",
|
|
26093
|
+
...raw ? [` praxis ${API_WORD} GET /model/v1/models`] : [],
|
|
26094
|
+
` ${sample ? example(sample) : "praxis --help"}`
|
|
26095
|
+
].join(`
|
|
26096
|
+
`);
|
|
26097
|
+
}
|
|
26098
|
+
function domains(commands) {
|
|
26099
|
+
return [...new Set(commands.map((c) => c.words[1]))].sort();
|
|
26100
|
+
}
|
|
26101
|
+
var workflows = [
|
|
26102
|
+
{ words: ["login"], usage: "login", summary: "Sign in with a browser device code." },
|
|
26103
|
+
{ words: ["logout"], usage: "logout", summary: "Remove the saved login from the keychain and fallback file." },
|
|
26104
|
+
{ words: ["whoami"], usage: "whoami", summary: "Show the signed-in identity and credential storage location." },
|
|
26105
|
+
{ words: ["org", "list"], usage: "org list", summary: "List organizations available to the login." },
|
|
26106
|
+
{ words: ["org", "switch"], usage: "org switch <organization_id>", summary: "Switch the login organization." },
|
|
26107
|
+
{ words: ["app", "create"], usage: "app create <name> [--dir <path>] [--template <dir>] [--no-install]", summary: "Create an application project from the standard template and install its dependencies.", example: "praxis app create my-app" },
|
|
26108
|
+
{ words: ["app", "deploy"], usage: "app deploy [--dir <path>] [--no-build]", summary: "Build the project, publish it, create declared secrets, run the health check and print the URL.", example: "praxis app deploy --dir ./my-app" },
|
|
26109
|
+
{ words: ["app", "rollback"], usage: "app rollback [--dir <path>] [--to <version_id>]", summary: "Deploy the previous version again (or --to a given version) and wait until it is active.", example: "praxis app rollback --to <version_id>" },
|
|
26110
|
+
{ words: ["skill"], usage: "skill [--install <dir>]", summary: "Print the AI skill, or write <dir>/praxis-cli/SKILL.md." }
|
|
26111
|
+
];
|
|
26112
|
+
var workflowNouns = [...new Set(workflows.filter((w) => w.words.length > 1).map((w) => w.words[0]))];
|
|
26113
|
+
function nounHelp(noun) {
|
|
26114
|
+
const mine = workflows.filter((w) => w.words[0] === noun);
|
|
26115
|
+
const width = Math.max(...mine.map((w) => w.usage.length));
|
|
26116
|
+
return [...mine.map((w) => ` praxis ${w.usage.padEnd(width)} ${w.summary}`), "", `Run \`praxis ${noun} <command> --help\` for a command's options.`, "", "Examples:", ...mine.map((w) => ` ${w.example ?? `praxis ${w.usage}`}`)].join(`
|
|
26117
|
+
`);
|
|
26118
|
+
}
|
|
26119
|
+
function workflowHelp(workflow, options) {
|
|
26120
|
+
return [
|
|
26121
|
+
`praxis ${workflow.usage}`,
|
|
26122
|
+
workflow.summary,
|
|
26123
|
+
"",
|
|
26124
|
+
"Options:",
|
|
26125
|
+
...[...options, ...globalOptions].map((s) => ` ${describeOption(s)}`),
|
|
25916
26126
|
" -h is an alias of --help",
|
|
25917
26127
|
"PRAXIS_ENV=develop|production (default: develop); no --env option.",
|
|
25918
26128
|
"",
|
|
25919
26129
|
"Examples:",
|
|
25920
|
-
` ${
|
|
26130
|
+
` ${workflow.example ?? `praxis ${workflow.usage}`}`
|
|
25921
26131
|
].join(`
|
|
25922
26132
|
`);
|
|
25923
26133
|
}
|
|
25924
26134
|
|
|
25925
26135
|
// src/config/env.ts
|
|
25926
26136
|
var DEFAULT_ENV = "develop";
|
|
26137
|
+
function validateApiUrl(value) {
|
|
26138
|
+
const fail = () => {
|
|
26139
|
+
throw new UsageError("PRAXIS_API_URL must be an https URL on deepraxis.com", "Use https://api.deepraxis.com or https://api-develop.deepraxis.com; credentials are never sent to other hosts.");
|
|
26140
|
+
};
|
|
26141
|
+
let u;
|
|
26142
|
+
try {
|
|
26143
|
+
u = new URL(value);
|
|
26144
|
+
} catch {
|
|
26145
|
+
return fail();
|
|
26146
|
+
}
|
|
26147
|
+
if (u.protocol !== "https:" || u.username || u.password || u.search || u.hash)
|
|
26148
|
+
fail();
|
|
26149
|
+
if (!(u.hostname === "api.deepraxis.com" || u.hostname.endsWith(".deepraxis.com")))
|
|
26150
|
+
fail();
|
|
26151
|
+
return value.replace(/\/+$/, "");
|
|
26152
|
+
}
|
|
26153
|
+
function inferEnvFromApiUrl(value) {
|
|
26154
|
+
return new URL(value).hostname.includes("-develop.") ? "develop" : "production";
|
|
26155
|
+
}
|
|
25927
26156
|
function environment(vars = process.env) {
|
|
25928
|
-
const
|
|
26157
|
+
const apiOverride = vars.PRAXIS_API_URL ? validateApiUrl(vars.PRAXIS_API_URL) : undefined;
|
|
26158
|
+
const env = vars.PRAXIS_ENV ?? (apiOverride ? inferEnvFromApiUrl(apiOverride) : DEFAULT_ENV);
|
|
25929
26159
|
if (env !== "develop" && env !== "production")
|
|
25930
26160
|
throw new UsageError("PRAXIS_ENV must be develop or production", "Set PRAXIS_ENV=develop or PRAXIS_ENV=production.");
|
|
25931
26161
|
const suffix = env === "develop" ? "-develop" : "";
|
|
@@ -25936,10 +26166,10 @@ function environment(vars = process.env) {
|
|
|
25936
26166
|
throw new Error;
|
|
25937
26167
|
return value.replace(/\/+$/, "");
|
|
25938
26168
|
} catch {
|
|
25939
|
-
throw new UsageError("Invalid PRAXIS endpoint URL", "Set
|
|
26169
|
+
throw new UsageError("Invalid PRAXIS endpoint URL", "Set PRAXIS_ACCOUNT_URL / PRAXIS_GATEWAY_URL to HTTP(S) host URLs.");
|
|
25940
26170
|
}
|
|
25941
26171
|
};
|
|
25942
|
-
return { env, apiUrl:
|
|
26172
|
+
return { env, apiUrl: apiOverride ?? `https://api${suffix}.deepraxis.com`, accountUrl: url(vars.PRAXIS_ACCOUNT_URL ?? `https://account${suffix}.deepraxis.com`), gatewayUrl: url(vars.PRAXIS_GATEWAY_URL ?? `https://gateway${suffix}.deepraxis.com`) };
|
|
25943
26173
|
}
|
|
25944
26174
|
|
|
25945
26175
|
// src/auth/token-store.ts
|
|
@@ -26169,6 +26399,9 @@ async function withLock(lockPath, run) {
|
|
|
26169
26399
|
}
|
|
26170
26400
|
|
|
26171
26401
|
// src/auth/credentials.ts
|
|
26402
|
+
function credentialSource(vars) {
|
|
26403
|
+
return vars.PRAXIS_API_KEY ? "api-key" : vars.PRAXIS_API_TOKEN ? "machine-token" : "login";
|
|
26404
|
+
}
|
|
26172
26405
|
async function identityRpc(config, fetchImpl, method, params, token) {
|
|
26173
26406
|
const response = await fetchImpl(`${config.gatewayUrl}/rpc/practice-identity`, {
|
|
26174
26407
|
method: "POST",
|
|
@@ -26198,15 +26431,20 @@ function credentials(config, vars, ports) {
|
|
|
26198
26431
|
const path = ports.path ?? tokenPath(config.env);
|
|
26199
26432
|
const store = ports.store ?? createTokenStore(config.env, path, ports.secretStore);
|
|
26200
26433
|
const apiKey = vars.PRAXIS_API_KEY;
|
|
26434
|
+
const machineToken = credentialSource(vars) === "machine-token" ? vars.PRAXIS_API_TOKEN : undefined;
|
|
26201
26435
|
const remember = ports.remember ?? (() => {});
|
|
26202
26436
|
if (apiKey)
|
|
26203
26437
|
remember(apiKey);
|
|
26438
|
+
if (machineToken)
|
|
26439
|
+
remember(machineToken);
|
|
26204
26440
|
let cached;
|
|
26205
26441
|
let cachedOrg;
|
|
26206
26442
|
let pending;
|
|
26207
26443
|
const requireLogin = () => {
|
|
26208
26444
|
if (apiKey)
|
|
26209
26445
|
throw new UsageError("The API Key determines the organization; listing or switching login organizations is unavailable.", "Unset PRAXIS_API_KEY and run `praxis login`, then retry the org command.");
|
|
26446
|
+
if (machineToken)
|
|
26447
|
+
throw new UsageError("The machine token determines the organization; listing or switching login organizations is unavailable.", "Unset PRAXIS_API_TOKEN and run `praxis login`, then retry the org command.");
|
|
26210
26448
|
};
|
|
26211
26449
|
async function exchange(organizationId) {
|
|
26212
26450
|
const stored = await store.load();
|
|
@@ -26231,6 +26469,11 @@ function credentials(config, vars, ports) {
|
|
|
26231
26469
|
throw new UsageError("--org cannot override an API Key organization", "Remove --org or unset PRAXIS_API_KEY and run `praxis login`.");
|
|
26232
26470
|
return { source: "api-key", token: apiKey };
|
|
26233
26471
|
}
|
|
26472
|
+
if (machineToken) {
|
|
26473
|
+
if (org)
|
|
26474
|
+
throw new UsageError("--org cannot override a machine token organization", "Remove --org or unset PRAXIS_API_TOKEN and run `praxis login`.");
|
|
26475
|
+
return { source: "machine-token", token: machineToken };
|
|
26476
|
+
}
|
|
26234
26477
|
const stored = await store.load();
|
|
26235
26478
|
const selected = org ?? stored?.organization_id;
|
|
26236
26479
|
const opaqueFresh = stored !== null && stored.expires_at - 30000 > ports.now();
|
|
@@ -26254,7 +26497,7 @@ function credentials(config, vars, ports) {
|
|
|
26254
26497
|
requireLogin,
|
|
26255
26498
|
async client(org) {
|
|
26256
26499
|
const c = await get(org);
|
|
26257
|
-
return new Praxis({ apiKey: c.token, baseUrl: config.apiUrl, fetch: c.source === "
|
|
26500
|
+
return new Praxis({ apiKey: c.token, baseUrl: config.apiUrl, fetch: c.source === "api-key" ? ports.fetch : jwtFetch(ports.fetch, c.token) });
|
|
26258
26501
|
},
|
|
26259
26502
|
async rpc(method, params = {}, org) {
|
|
26260
26503
|
const c = await get(org);
|
|
@@ -26287,7 +26530,7 @@ function commandSummary(command) {
|
|
|
26287
26530
|
const chars = [...description];
|
|
26288
26531
|
return chars.length > 120 ? chars.slice(0, 119).join("") + "…" : description;
|
|
26289
26532
|
}
|
|
26290
|
-
const words =
|
|
26533
|
+
const words = command.words.slice(1);
|
|
26291
26534
|
const verb = (words.pop() ?? "use").replace(/-/g, " ");
|
|
26292
26535
|
if (verb === "list" && words.length && !words.at(-1).endsWith("s"))
|
|
26293
26536
|
words[words.length - 1] += "s";
|
|
@@ -26305,7 +26548,7 @@ function skillDocument(commands) {
|
|
|
26305
26548
|
"## General rules",
|
|
26306
26549
|
"",
|
|
26307
26550
|
"- Select the environment only with `PRAXIS_ENV`; there is no `--env` option. The dev package defaults to develop.",
|
|
26308
|
-
"- Authenticate with `PRAXIS_API_KEY` or `praxis login
|
|
26551
|
+
"- Authenticate with `PRAXIS_API_KEY`, `PRAXIS_API_TOKEN` (a machine token, sent in the Authorization header; computers have it set) or `praxis login`, in that order. API keys and machine tokens determine the organization; login sessions can use `--org <organization_id>`. `PRAXIS_API_URL` overrides the API base and must be an https deepraxis.com URL.",
|
|
26309
26552
|
"- AI mode is enabled automatically in supported agent environments. Use `--format agent` to select it explicitly.",
|
|
26310
26553
|
"- Use `--json` for the complete JSON response without presentation truncation. Successful JSON output goes to stdout; errors go to stderr.",
|
|
26311
26554
|
"- Supply bodies with `--data JSON`, `--data @file.json`, or `--data -` for stdin. Scalar field options override matching JSON fields. Structured fields require `--data`.",
|
|
@@ -26313,21 +26556,17 @@ function skillDocument(commands) {
|
|
|
26313
26556
|
"- Use `--limit N` to limit a server page for SDK-paginated commands. Other lists apply it only to displayed rows; `--param-limit` still sends the API parameter when available. `--all` follows supported pagination and cannot be combined with `--limit`.",
|
|
26314
26557
|
"- Error JSON has an `error` object: `code` identifies the failure, `message` describes it, `hint` suggests recovery, `env` identifies the environment, `command` identifies the command, `status` is the HTTP status, and `request_id` correlates diagnostics. Unavailable fields are null. Exit codes: 0 success, 1 runtime/API error, 2 usage error.",
|
|
26315
26558
|
"- Append `--help` to a command for its optional parameters. Required scalar body options below can also be supplied as fields in `--data`.",
|
|
26559
|
+
"- Two layers: top-level commands (`login`, `org`, `skill`, …) are workflows and session commands; every API operation is under `praxis api <domain> …`. `praxis api <METHOD> <path>` sends a raw request (the method is uppercase).",
|
|
26316
26560
|
"",
|
|
26317
26561
|
"## Account and CLI",
|
|
26318
26562
|
"",
|
|
26319
|
-
|
|
26320
|
-
"praxis
|
|
26321
|
-
"praxis whoami — Show the signed-in identity and credential storage location.",
|
|
26322
|
-
"praxis org list — List organizations available to the login.",
|
|
26323
|
-
"praxis org switch <organization_id> — Switch the login organization.",
|
|
26324
|
-
"praxis api <METHOD> <path> — Call an API path directly. (--data for a request body)",
|
|
26325
|
-
"praxis skill — Print this skill; use --install <dir> to write <dir>/praxis-cli/SKILL.md."
|
|
26563
|
+
...workflows.map((w) => `praxis ${w.usage} — ${w.summary.replace("Print the AI skill, or write", "Print this skill; use --install <dir> to write")}`),
|
|
26564
|
+
"praxis api <METHOD> <path> — Call an API path directly. (--data for a request body)"
|
|
26326
26565
|
];
|
|
26327
26566
|
let domain;
|
|
26328
26567
|
for (const command of commands) {
|
|
26329
|
-
if (command
|
|
26330
|
-
domain = command
|
|
26568
|
+
if (domainOf(command) !== domain) {
|
|
26569
|
+
domain = domainOf(command);
|
|
26331
26570
|
lines.push("", `## ${domain}`, "");
|
|
26332
26571
|
}
|
|
26333
26572
|
const target = targetParam(command.op);
|
|
@@ -26458,7 +26697,7 @@ async function whoami(auth, env, org) {
|
|
|
26458
26697
|
source: credential.source,
|
|
26459
26698
|
...credential.storedIn ? { "stored in": credential.storedIn } : {},
|
|
26460
26699
|
expires_at: credential.expiresAt ? new Date(credential.expiresAt).toISOString() : null,
|
|
26461
|
-
...credential.source === "api-key" ? { note: "The API key determines the organization." } : {}
|
|
26700
|
+
...credential.source === "api-key" ? { note: "The API key determines the organization." } : credential.source === "machine-token" ? { note: "The machine token determines the organization." } : {}
|
|
26462
26701
|
};
|
|
26463
26702
|
}
|
|
26464
26703
|
|
|
@@ -26507,6 +26746,365 @@ async function api(config, auth, method, path, body, fetchImpl, emit, org) {
|
|
|
26507
26746
|
emit({ kind: "object", data: result });
|
|
26508
26747
|
}
|
|
26509
26748
|
|
|
26749
|
+
// src/commands/app.ts
|
|
26750
|
+
import { cpSync, existsSync as existsSync2, mkdirSync as mkdirSync2, mkdtempSync, readdirSync, readFileSync as readFileSync2, renameSync, rmSync as rmSync2, statSync as statSync2, writeFileSync as writeFileSync2 } from "node:fs";
|
|
26751
|
+
import { tmpdir } from "node:os";
|
|
26752
|
+
import { dirname, join as join3, resolve } from "node:path";
|
|
26753
|
+
|
|
26754
|
+
// src/app/pure.ts
|
|
26755
|
+
import { createHash } from "node:crypto";
|
|
26756
|
+
var WORKER_NAME = /^(?!(?:www|api|admin|app|canary|canary-probe|status|docs|console)$)(?!.*--)[a-z0-9][a-z0-9-]{1,38}[a-z0-9]$/;
|
|
26757
|
+
function validateAppName(name) {
|
|
26758
|
+
if (!WORKER_NAME.test(name))
|
|
26759
|
+
throw new UsageError(`Invalid app name: ${name}`, "Use 3-40 lowercase letters, digits and single hyphens, starting and ending with a letter or digit; www, api, admin, app, canary, canary-probe, status, docs and console are reserved.");
|
|
26760
|
+
return name;
|
|
26761
|
+
}
|
|
26762
|
+
function stringEnd(text, i) {
|
|
26763
|
+
let j = i + 1;
|
|
26764
|
+
while (j < text.length && text[j] !== '"')
|
|
26765
|
+
j += text[j] === "\\" ? 2 : 1;
|
|
26766
|
+
return j;
|
|
26767
|
+
}
|
|
26768
|
+
function parseJsonc(text) {
|
|
26769
|
+
let out = "";
|
|
26770
|
+
for (let i = 0;i < text.length; i++) {
|
|
26771
|
+
const c = text[i];
|
|
26772
|
+
if (c === '"') {
|
|
26773
|
+
const j = stringEnd(text, i);
|
|
26774
|
+
out += text.slice(i, j + 1);
|
|
26775
|
+
i = j;
|
|
26776
|
+
continue;
|
|
26777
|
+
}
|
|
26778
|
+
if (c === "/" && text[i + 1] === "/") {
|
|
26779
|
+
while (i < text.length && text[i] !== `
|
|
26780
|
+
`)
|
|
26781
|
+
i++;
|
|
26782
|
+
out += `
|
|
26783
|
+
`;
|
|
26784
|
+
continue;
|
|
26785
|
+
}
|
|
26786
|
+
if (c === "/" && text[i + 1] === "*") {
|
|
26787
|
+
const end = text.indexOf("*/", i + 2);
|
|
26788
|
+
if (end < 0)
|
|
26789
|
+
throw new SyntaxError("Unterminated block comment");
|
|
26790
|
+
i = end + 1;
|
|
26791
|
+
out += " ";
|
|
26792
|
+
continue;
|
|
26793
|
+
}
|
|
26794
|
+
out += c;
|
|
26795
|
+
}
|
|
26796
|
+
let result = "";
|
|
26797
|
+
for (let i = 0;i < out.length; i++) {
|
|
26798
|
+
const c = out[i];
|
|
26799
|
+
if (c === '"') {
|
|
26800
|
+
const j = stringEnd(out, i);
|
|
26801
|
+
result += out.slice(i, j + 1);
|
|
26802
|
+
i = j;
|
|
26803
|
+
continue;
|
|
26804
|
+
}
|
|
26805
|
+
if (c === "," && /^\s*[}\]]/.test(out.slice(i + 1)))
|
|
26806
|
+
continue;
|
|
26807
|
+
result += c;
|
|
26808
|
+
}
|
|
26809
|
+
return JSON.parse(result);
|
|
26810
|
+
}
|
|
26811
|
+
function parseProject(value) {
|
|
26812
|
+
const fail = (m) => {
|
|
26813
|
+
throw new UsageError(m, 'praxis.jsonc needs {"kind":"application","name":"<app-name>"}; `praxis app create` writes it.');
|
|
26814
|
+
};
|
|
26815
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
26816
|
+
return fail("praxis.jsonc must be an object");
|
|
26817
|
+
const v = value;
|
|
26818
|
+
if (v.kind !== "application")
|
|
26819
|
+
fail('praxis.jsonc kind must be "application"');
|
|
26820
|
+
if (typeof v.name !== "string" || !v.name)
|
|
26821
|
+
fail("praxis.jsonc has no name");
|
|
26822
|
+
const name = validateAppName(v.name);
|
|
26823
|
+
const secrets = {};
|
|
26824
|
+
if (v.secrets !== undefined) {
|
|
26825
|
+
if (!v.secrets || typeof v.secrets !== "object" || Array.isArray(v.secrets))
|
|
26826
|
+
fail("praxis.jsonc secrets must be an object");
|
|
26827
|
+
for (const [key, how] of Object.entries(v.secrets)) {
|
|
26828
|
+
if (!/^[A-Za-z_][A-Za-z0-9_]{0,63}$/.test(key))
|
|
26829
|
+
fail(`Invalid secret name: ${key}`);
|
|
26830
|
+
if (how !== "generate")
|
|
26831
|
+
fail(`Secret ${key}: only "generate" is supported`);
|
|
26832
|
+
secrets[key] = "generate";
|
|
26833
|
+
}
|
|
26834
|
+
}
|
|
26835
|
+
if (v.healthcheck !== undefined && (typeof v.healthcheck !== "string" || !v.healthcheck.startsWith("/") || v.healthcheck.startsWith("//")))
|
|
26836
|
+
fail("praxis.jsonc healthcheck must be a path starting with /");
|
|
26837
|
+
return { name, secrets, ...typeof v.healthcheck === "string" ? { healthcheck: v.healthcheck } : {} };
|
|
26838
|
+
}
|
|
26839
|
+
function planSecrets(existing, declared, generate) {
|
|
26840
|
+
const have = new Set(existing);
|
|
26841
|
+
const bindings = [...have].map((name) => ({ type: "secret_text", name }));
|
|
26842
|
+
const created = [];
|
|
26843
|
+
for (const name of Object.keys(declared))
|
|
26844
|
+
if (!have.has(name)) {
|
|
26845
|
+
bindings.push({ type: "secret_text", name, text: generate() });
|
|
26846
|
+
created.push(name);
|
|
26847
|
+
}
|
|
26848
|
+
return { bindings, created };
|
|
26849
|
+
}
|
|
26850
|
+
function chooseRollbackTarget(deployments, currentVersionId) {
|
|
26851
|
+
for (const d of deployments) {
|
|
26852
|
+
const id = d.versions[0]?.version_id;
|
|
26853
|
+
if (id && id !== currentVersionId)
|
|
26854
|
+
return id;
|
|
26855
|
+
}
|
|
26856
|
+
return;
|
|
26857
|
+
}
|
|
26858
|
+
function verifyIntegrity(bytes, dist) {
|
|
26859
|
+
if (dist.integrity) {
|
|
26860
|
+
return dist.integrity.split(/\s+/).some((entry) => {
|
|
26861
|
+
const at = entry.indexOf("-");
|
|
26862
|
+
return entry.slice(0, at) === "sha512" && createHash("sha512").update(bytes).digest("base64") === entry.slice(at + 1);
|
|
26863
|
+
});
|
|
26864
|
+
}
|
|
26865
|
+
if (dist.shasum)
|
|
26866
|
+
return createHash("sha1").update(bytes).digest("hex") === dist.shasum;
|
|
26867
|
+
return false;
|
|
26868
|
+
}
|
|
26869
|
+
var templateTag = (env) => env === "develop" ? "dev" : "latest";
|
|
26870
|
+
function randomSecret(bytes = 32) {
|
|
26871
|
+
return Buffer.from(crypto.getRandomValues(new Uint8Array(bytes))).toString("base64url");
|
|
26872
|
+
}
|
|
26873
|
+
function decideBuildOutput(mode) {
|
|
26874
|
+
return mode === "human" ? "stream" : "capture";
|
|
26875
|
+
}
|
|
26876
|
+
function tailLines(text, count3) {
|
|
26877
|
+
return text.replace(/\s+$/, "").split(`
|
|
26878
|
+
`).slice(-count3).join(`
|
|
26879
|
+
`);
|
|
26880
|
+
}
|
|
26881
|
+
function nextSteps(dir, bunAvailable) {
|
|
26882
|
+
return [`cd ${dir}`, bunAvailable ? "bun run dev" : "npm run dev", "praxis app deploy"];
|
|
26883
|
+
}
|
|
26884
|
+
|
|
26885
|
+
// src/commands/app.ts
|
|
26886
|
+
var REGISTRY = "https://registry.npmjs.org/@deepraxis%2fapp-template";
|
|
26887
|
+
async function downloadTemplate(env, into, ports) {
|
|
26888
|
+
const meta = await ports.fetch(REGISTRY, { headers: { accept: "application/vnd.npm.install-v1+json" }, signal: AbortSignal.timeout(30000) });
|
|
26889
|
+
if (!meta.ok)
|
|
26890
|
+
throw new PraxisError({ code: "TEMPLATE_UNAVAILABLE", message: `Could not read the template package from the npm registry (${meta.status})` });
|
|
26891
|
+
const doc = await meta.json();
|
|
26892
|
+
const tag = templateTag(env), version = doc["dist-tags"]?.[tag];
|
|
26893
|
+
const dist = version ? doc.versions?.[version]?.dist : undefined;
|
|
26894
|
+
if (!version || !dist?.tarball)
|
|
26895
|
+
throw new PraxisError({ code: "TEMPLATE_UNAVAILABLE", message: `@deepraxis/app-template has no "${tag}" release` });
|
|
26896
|
+
const tarball = new URL(dist.tarball);
|
|
26897
|
+
if (tarball.protocol !== "https:")
|
|
26898
|
+
throw new PraxisError({ code: "TEMPLATE_UNAVAILABLE", message: "Template tarball is not served over https" });
|
|
26899
|
+
const res = await ports.fetch(tarball, { signal: AbortSignal.timeout(60000) });
|
|
26900
|
+
if (!res.ok)
|
|
26901
|
+
throw new PraxisError({ code: "TEMPLATE_UNAVAILABLE", message: `Template download failed (${res.status})` });
|
|
26902
|
+
const bytes = new Uint8Array(await res.arrayBuffer());
|
|
26903
|
+
if (!verifyIntegrity(bytes, dist))
|
|
26904
|
+
throw new PraxisError({ code: "TEMPLATE_INTEGRITY", message: `Template ${version} failed its integrity check; nothing was extracted` });
|
|
26905
|
+
const work = mkdtempSync(join3(tmpdir(), "praxis-template-"));
|
|
26906
|
+
try {
|
|
26907
|
+
writeFileSync2(join3(work, "template.tgz"), bytes);
|
|
26908
|
+
const code = await ports.spawn("tar", ["-xzf", "template.tgz", "package/template"], { cwd: work });
|
|
26909
|
+
if (code !== 0 || !existsSync2(join3(work, "package", "template")))
|
|
26910
|
+
throw new PraxisError({ code: "TEMPLATE_UNAVAILABLE", message: "Could not extract package/template from the template tarball" });
|
|
26911
|
+
cpSync(join3(work, "package", "template"), into, { recursive: true });
|
|
26912
|
+
} finally {
|
|
26913
|
+
rmSync2(work, { recursive: true, force: true });
|
|
26914
|
+
}
|
|
26915
|
+
return { version };
|
|
26916
|
+
}
|
|
26917
|
+
async function appCreate(args, env, ports) {
|
|
26918
|
+
const name = validateAppName(args.name);
|
|
26919
|
+
const dir = resolve(ports.cwd, args.dir ?? name);
|
|
26920
|
+
if (existsSync2(dir) && (!statSync2(dir).isDirectory() || readdirSync(dir).length))
|
|
26921
|
+
throw new UsageError(`${dir} already exists and is not empty`, "Pick another name or --dir.");
|
|
26922
|
+
mkdirSync2(dir, { recursive: true });
|
|
26923
|
+
let version = "local";
|
|
26924
|
+
try {
|
|
26925
|
+
if (args.template) {
|
|
26926
|
+
const source = resolve(ports.cwd, args.template);
|
|
26927
|
+
if (!existsSync2(source) || !statSync2(source).isDirectory())
|
|
26928
|
+
throw new UsageError(`--template ${source} is not a directory`);
|
|
26929
|
+
cpSync(source, dir, { recursive: true });
|
|
26930
|
+
} else
|
|
26931
|
+
version = (await downloadTemplate(env, dir, ports)).version;
|
|
26932
|
+
if (existsSync2(join3(dir, "_gitignore")))
|
|
26933
|
+
renameSync(join3(dir, "_gitignore"), join3(dir, ".gitignore"));
|
|
26934
|
+
const jsonc = join3(dir, "praxis.jsonc");
|
|
26935
|
+
if (existsSync2(jsonc))
|
|
26936
|
+
writeFileSync2(jsonc, readFileSync2(jsonc, "utf8").replaceAll("<app-name>", name));
|
|
26937
|
+
const pkg = join3(dir, "package.json");
|
|
26938
|
+
if (existsSync2(pkg))
|
|
26939
|
+
writeFileSync2(pkg, JSON.stringify({ ...JSON.parse(readFileSync2(pkg, "utf8")), name }, null, 2) + `
|
|
26940
|
+
`);
|
|
26941
|
+
} catch (error) {
|
|
26942
|
+
rmSync2(dir, { recursive: true, force: true });
|
|
26943
|
+
throw error;
|
|
26944
|
+
}
|
|
26945
|
+
if (args.install) {
|
|
26946
|
+
const pm = ports.has("bun") ? "bun" : "npm";
|
|
26947
|
+
const code = await ports.spawn(pm, ["install"], { cwd: dir });
|
|
26948
|
+
if (code !== 0)
|
|
26949
|
+
throw new PraxisError({ code: "INSTALL_FAILED", message: `${pm} install failed (exit ${code}); the project was created in ${dir}. Run it again there.` });
|
|
26950
|
+
}
|
|
26951
|
+
const next_steps = nextSteps(dir, ports.has("bun"));
|
|
26952
|
+
ports.human(`Created ${dir}
|
|
26953
|
+
|
|
26954
|
+
Next steps:
|
|
26955
|
+
${next_steps.map((step) => ` ${step}`).join(`
|
|
26956
|
+
`)}`);
|
|
26957
|
+
ports.emit({ kind: "object", data: { dir, name, template_version: version, next_steps } });
|
|
26958
|
+
}
|
|
26959
|
+
function findProjectDir(dir, cwd) {
|
|
26960
|
+
if (dir) {
|
|
26961
|
+
const d = resolve(cwd, dir);
|
|
26962
|
+
if (!existsSync2(join3(d, "praxis.jsonc")))
|
|
26963
|
+
throw new UsageError(`No praxis.jsonc in ${d}`, "Run inside a project made by `praxis app create`, or pass --dir.");
|
|
26964
|
+
return d;
|
|
26965
|
+
}
|
|
26966
|
+
for (let d = resolve(cwd);; d = dirname(d)) {
|
|
26967
|
+
if (existsSync2(join3(d, "praxis.jsonc")))
|
|
26968
|
+
return d;
|
|
26969
|
+
if (dirname(d) === d)
|
|
26970
|
+
throw new UsageError("No praxis.jsonc found in this directory or its parents", "Run inside a project made by `praxis app create`, or pass --dir.");
|
|
26971
|
+
}
|
|
26972
|
+
}
|
|
26973
|
+
function loadProject(dir) {
|
|
26974
|
+
let value;
|
|
26975
|
+
try {
|
|
26976
|
+
value = parseJsonc(readFileSync2(join3(dir, "praxis.jsonc"), "utf8"));
|
|
26977
|
+
} catch (error) {
|
|
26978
|
+
throw new UsageError(`praxis.jsonc is not valid JSONC: ${error.message}`);
|
|
26979
|
+
}
|
|
26980
|
+
return parseProject(value);
|
|
26981
|
+
}
|
|
26982
|
+
function readTree(root) {
|
|
26983
|
+
const files = {};
|
|
26984
|
+
const walk = (rel) => {
|
|
26985
|
+
for (const entry of readdirSync(join3(root, rel), { withFileTypes: true })) {
|
|
26986
|
+
const path = rel ? `${rel}/${entry.name}` : entry.name;
|
|
26987
|
+
if (entry.isDirectory())
|
|
26988
|
+
walk(path);
|
|
26989
|
+
else if (entry.isFile())
|
|
26990
|
+
files[path] = new Uint8Array(readFileSync2(join3(root, path)));
|
|
26991
|
+
}
|
|
26992
|
+
};
|
|
26993
|
+
walk("");
|
|
26994
|
+
return files;
|
|
26995
|
+
}
|
|
26996
|
+
async function findWorker(client2, name) {
|
|
26997
|
+
for await (const w of await client2.application.workers.list())
|
|
26998
|
+
if (w.name === name)
|
|
26999
|
+
return w;
|
|
27000
|
+
return;
|
|
27001
|
+
}
|
|
27002
|
+
async function waitActive(client2, workerId, deployment, ports, timeoutMs = 120000) {
|
|
27003
|
+
const deadline = ports.now() + timeoutMs;
|
|
27004
|
+
let current = deployment;
|
|
27005
|
+
while (current.praxis.status !== "active") {
|
|
27006
|
+
if (current.praxis.status === "failed" || current.praxis.status === "cancelled")
|
|
27007
|
+
throw new PraxisError({ code: current.praxis.error?.code ?? "DEPLOYMENT_FAILED", message: current.praxis.error?.message ?? `Deployment ${current.id} ${current.praxis.status}` });
|
|
27008
|
+
if (ports.now() >= deadline)
|
|
27009
|
+
throw new PraxisError({ code: "DEPLOY_TIMEOUT", message: `Deployment ${current.id} did not become active within ${timeoutMs} ms; it may still be running` });
|
|
27010
|
+
await ports.wait(1000);
|
|
27011
|
+
current = await client2.application.workers.deployments.retrieve({ path: { worker_id: workerId, deployment_id: current.id } });
|
|
27012
|
+
}
|
|
27013
|
+
return current;
|
|
27014
|
+
}
|
|
27015
|
+
async function healthCheck2(url, path, ports, timeoutMs = 60000) {
|
|
27016
|
+
const target = url.replace(/\/+$/, "") + path;
|
|
27017
|
+
const deadline = ports.now() + timeoutMs;
|
|
27018
|
+
let last = "no response";
|
|
27019
|
+
for (;; ) {
|
|
27020
|
+
try {
|
|
27021
|
+
const res = await ports.fetch(target, { redirect: "manual", signal: AbortSignal.timeout(1e4) });
|
|
27022
|
+
if (res.status >= 200 && res.status < 300)
|
|
27023
|
+
return { path, status: res.status };
|
|
27024
|
+
last = String(res.status);
|
|
27025
|
+
} catch (error) {
|
|
27026
|
+
last = error.message;
|
|
27027
|
+
}
|
|
27028
|
+
if (ports.now() >= deadline)
|
|
27029
|
+
throw new PraxisError({ code: "HEALTHCHECK_FAILED", message: `Health check failed: GET ${target} answered ${last} for ${Math.round(timeoutMs / 1000)}s` });
|
|
27030
|
+
await ports.wait(2000);
|
|
27031
|
+
}
|
|
27032
|
+
}
|
|
27033
|
+
async function runBuild(pm, dir, ports) {
|
|
27034
|
+
if (decideBuildOutput(ports.mode) === "stream")
|
|
27035
|
+
return ports.spawn(pm, ["run", "build"], { cwd: dir });
|
|
27036
|
+
const started = ports.now();
|
|
27037
|
+
const { code, output } = await ports.spawnCapture(pm, ["run", "build"], { cwd: dir });
|
|
27038
|
+
const text = code === 0 ? `build ok in ${Math.round((ports.now() - started) / 100) / 10}s` : tailLines(output, 60);
|
|
27039
|
+
ports.emit({ kind: "message", channel: "stderr", text });
|
|
27040
|
+
return code;
|
|
27041
|
+
}
|
|
27042
|
+
async function appDeploy(args, client2, ports) {
|
|
27043
|
+
const dir = findProjectDir(args.dir, ports.cwd);
|
|
27044
|
+
const project = loadProject(dir);
|
|
27045
|
+
if (args.build) {
|
|
27046
|
+
const pm = ports.has("bun") ? "bun" : "npm";
|
|
27047
|
+
const code = await runBuild(pm, dir, ports);
|
|
27048
|
+
if (code !== 0)
|
|
27049
|
+
throw new PraxisError({ code: "BUILD_FAILED", message: `${pm} run build failed (exit ${code})` });
|
|
27050
|
+
}
|
|
27051
|
+
const server = join3(dir, "dist", "server"), clientDir = join3(dir, "dist", "client");
|
|
27052
|
+
if (!existsSync2(join3(server, "wrangler.json")) || !existsSync2(clientDir))
|
|
27053
|
+
throw new UsageError("No build output in dist/server and dist/client", "Run without --no-build, or build the project first.");
|
|
27054
|
+
const serverFiles = readTree(server);
|
|
27055
|
+
let input;
|
|
27056
|
+
try {
|
|
27057
|
+
input = publishInputFromBuild({ serverFiles, clientFiles: readTree(clientDir), wranglerJson: parseJsonc(new TextDecoder().decode(serverFiles["wrangler.json"])) }, project.name);
|
|
27058
|
+
} catch (error) {
|
|
27059
|
+
if (error instanceof PraxisError && error.code === "UNSUPPORTED")
|
|
27060
|
+
throw new PraxisError({ code: "UNSUPPORTED", message: `${error.message}. Remove those bindings from wrangler.jsonc and rebuild.` });
|
|
27061
|
+
throw error;
|
|
27062
|
+
}
|
|
27063
|
+
const worker = await findWorker(client2, project.name);
|
|
27064
|
+
const existing = worker ? (await client2.application.workers.secrets.list({ path: { worker_id: worker.id } })).map((s) => s.name) : [];
|
|
27065
|
+
const { bindings, created } = planSecrets(existing, project.secrets, ports.random);
|
|
27066
|
+
const named = new Set(bindings.map((b) => b.name));
|
|
27067
|
+
input = { ...input, bindings: [...(input.bindings ?? []).filter((b) => !(b.type === "secret_text" && named.has(b.name))), ...bindings], wait: true };
|
|
27068
|
+
const result = await client2.application.publish(input);
|
|
27069
|
+
let health = null;
|
|
27070
|
+
if (project.healthcheck)
|
|
27071
|
+
health = await healthCheck2(result.url, project.healthcheck, ports);
|
|
27072
|
+
ports.emit({ kind: "object", data: {
|
|
27073
|
+
url: result.url,
|
|
27074
|
+
worker_id: result.worker_id,
|
|
27075
|
+
version_id: result.version_id,
|
|
27076
|
+
deployment_id: result.deployment_id,
|
|
27077
|
+
uploaded: result.uploaded,
|
|
27078
|
+
reused: result.reused,
|
|
27079
|
+
secrets_created: created,
|
|
27080
|
+
health: health ? `${health.status} ${health.path}` : "not configured"
|
|
27081
|
+
} });
|
|
27082
|
+
}
|
|
27083
|
+
async function appRollback(args, client2, ports) {
|
|
27084
|
+
const project = loadProject(findProjectDir(args.dir, ports.cwd));
|
|
27085
|
+
const worker = await findWorker(client2, project.name);
|
|
27086
|
+
if (!worker)
|
|
27087
|
+
throw new PraxisError({ code: "NOT_FOUND", message: `No application named ${project.name}; deploy it first with \`praxis app deploy\`` });
|
|
27088
|
+
const deployments = [];
|
|
27089
|
+
for await (const d of await client2.application.workers.deployments.list({ path: { worker_id: worker.id } }))
|
|
27090
|
+
deployments.push(d);
|
|
27091
|
+
const active = deployments.find((d) => d.id === worker.praxis?.current_deployment_id) ?? deployments.find((d) => d.praxis.status === "active");
|
|
27092
|
+
const from = active?.versions[0]?.version_id;
|
|
27093
|
+
if (!from)
|
|
27094
|
+
throw new PraxisError({ code: "NOT_FOUND", message: `${project.name} has no active deployment` });
|
|
27095
|
+
const to = args.to ?? chooseRollbackTarget(deployments, from);
|
|
27096
|
+
if (!to)
|
|
27097
|
+
throw new UsageError(`${project.name} has no earlier version to roll back to`, "Deploy a second version first, or pass --to <version_id>.");
|
|
27098
|
+
if (to === from)
|
|
27099
|
+
throw new UsageError(`Version ${to} is already active`);
|
|
27100
|
+
const created = await client2.application.workers.deployments.create({ path: { worker_id: worker.id }, body: { strategy: "percentage", versions: [{ version_id: to, percentage: 100 }] } });
|
|
27101
|
+
const done = await waitActive(client2, worker.id, created, ports);
|
|
27102
|
+
ports.emit({ kind: "object", data: { url: worker.subdomain?.url ?? null, worker_id: worker.id, from_version: from, to_version: to, deployment_id: done.id } });
|
|
27103
|
+
}
|
|
27104
|
+
|
|
27105
|
+
// src/main.ts
|
|
27106
|
+
import { spawnSync } from "node:child_process";
|
|
27107
|
+
|
|
26510
27108
|
// src/core/confirm.ts
|
|
26511
27109
|
function destructive(method) {
|
|
26512
27110
|
return /^(delete|purge|remove)(?:[A-Z-]|$)/.test(method);
|
|
@@ -26662,6 +27260,21 @@ function plain(value) {
|
|
|
26662
27260
|
` || c === "\r" || c === "\t" ? " " : "");
|
|
26663
27261
|
}
|
|
26664
27262
|
|
|
27263
|
+
// src/output/list-columns.ts
|
|
27264
|
+
function workerStatus(item) {
|
|
27265
|
+
const row = record(item), status = record(row.praxis).status;
|
|
27266
|
+
if (typeof status !== "string")
|
|
27267
|
+
return item;
|
|
27268
|
+
return { ...row, status };
|
|
27269
|
+
}
|
|
27270
|
+
var listProjections = {
|
|
27271
|
+
"application.workers.list": workerStatus
|
|
27272
|
+
};
|
|
27273
|
+
function projectList(items, op) {
|
|
27274
|
+
const project = op ? listProjections[op] : undefined;
|
|
27275
|
+
return project ? items.map(project) : items;
|
|
27276
|
+
}
|
|
27277
|
+
|
|
26665
27278
|
// src/output/human.ts
|
|
26666
27279
|
var count3 = (n, noun) => `${n} ${noun}${n === 1 ? "" : "s"}`;
|
|
26667
27280
|
function identity(result, ctx) {
|
|
@@ -26730,7 +27343,7 @@ function table(items, ctx, paint) {
|
|
|
26730
27343
|
const text = truncate(v, sizes[i]);
|
|
26731
27344
|
let styled = header ? paint.dim(text) : text;
|
|
26732
27345
|
if (!header && ["status", "state"].includes(columns[i])) {
|
|
26733
|
-
const color = /^(running|active|ready|succeeded|completed|enabled)$/i.test(v) ? paint.green : /^(pending|queued|starting|creating|paused)$/i.test(v) ? paint.yellow : /^(failed|error|cancelled|expired|disabled)$/i.test(v) ? paint.red : (s) => s;
|
|
27346
|
+
const color = /^(running|active|ready|succeeded|completed|enabled)$/i.test(v) ? paint.green : /^(pending|queued|starting|creating|paused)$/i.test(v) ? paint.yellow : /^(failed|error|cancelled|expired|disabled|deleted|purging|purged|suspended)$/i.test(v) ? paint.red : (s) => s;
|
|
26734
27347
|
styled = color(text);
|
|
26735
27348
|
}
|
|
26736
27349
|
return styled + (i === row.length - 1 ? "" : " ".repeat(Math.max(0, sizes[i] - width(text))));
|
|
@@ -26790,7 +27403,7 @@ function renderHuman(result, ctx) {
|
|
|
26790
27403
|
` : "") };
|
|
26791
27404
|
case "list":
|
|
26792
27405
|
return {
|
|
26793
|
-
stdout: result.items.length ? table(result.items, ctx, out) : out.dim(`No ${plain(resource)}${/(s|x|ch|sh)$/.test(resource) ? "es" : "s"}.`) + `
|
|
27406
|
+
stdout: result.items.length ? table(projectList(result.items, result.op), ctx, out) : out.dim(`No ${plain(resource)}${/(s|x|ch|sh)$/.test(resource) ? "es" : "s"}.`) + `
|
|
26794
27407
|
`,
|
|
26795
27408
|
stderr: err.dim(`${result.total !== undefined ? `${result.items.length} of ${result.total} items` : count3(result.items.length, "item")}${result.nextCursor ? ` · next: ${ctx.nextCommand ?? "praxis --help"}` : ""}`) + `
|
|
26796
27409
|
`
|
|
@@ -26862,8 +27475,8 @@ ${usage ? `usage: ${JSON.stringify(usage)}
|
|
|
26862
27475
|
return { stdout, stderr };
|
|
26863
27476
|
}
|
|
26864
27477
|
case "list": {
|
|
26865
|
-
const
|
|
26866
|
-
const lines = columns.length ? ["|" + columns.map(cell).join("|") + "|", "|" + columns.map(() => "---").join("|") + "|", ...
|
|
27478
|
+
const items = projectList(result.items, result.op), columns = columnsFor(items);
|
|
27479
|
+
const lines = columns.length ? ["|" + columns.map(cell).join("|") + "|", "|" + columns.map(() => "---").join("|") + "|", ...items.map((item) => "|" + columns.map((key) => cell(timestamp(key, record(item)[key]) ?? scalar(record(item)[key]))).join("|") + "|")] : [];
|
|
26867
27480
|
lines.push(result.total !== undefined ? `${result.items.length} of ${result.total} rows; more: \`${ctx.moreCommand ?? "praxis --help"}\`` : `${result.items.length} rows; ${result.nextCursor ? `more: \`${ctx.nextCommand ?? "praxis --help"}\`` : "end"}`);
|
|
26868
27481
|
return { stdout: lines.join(`
|
|
26869
27482
|
`) + `
|
|
@@ -27072,9 +27685,9 @@ function startProgress(ctx, write = (text) => writeOutput({ stderr: text })) {
|
|
|
27072
27685
|
}
|
|
27073
27686
|
|
|
27074
27687
|
// src/main.ts
|
|
27075
|
-
var version = "0.
|
|
27688
|
+
var version = "0.2.0-dev.3c36146";
|
|
27076
27689
|
var commands = commandTree(operations);
|
|
27077
|
-
var manual = [
|
|
27690
|
+
var manual = [...workflows.map((w) => w.words), [API_WORD]];
|
|
27078
27691
|
function route(argv) {
|
|
27079
27692
|
const words = [], args = [];
|
|
27080
27693
|
for (let i = 0;i < argv.length; i++) {
|
|
@@ -27090,7 +27703,11 @@ function route(argv) {
|
|
|
27090
27703
|
continue;
|
|
27091
27704
|
}
|
|
27092
27705
|
words.push(arg);
|
|
27706
|
+
if (words.length === 1 && words[0] === API_WORD && HTTP_METHOD.test(argv[i + 1] ?? ""))
|
|
27707
|
+
return { words, args: [...args, ...argv.slice(i + 1)] };
|
|
27093
27708
|
const command = commands.find((c) => c.words.length === words.length && c.words.every((w, n) => w === words[n]));
|
|
27709
|
+
if (words.length === 1 && words[0] === API_WORD && argv[i + 1] !== undefined && !argv[i + 1].startsWith("-"))
|
|
27710
|
+
continue;
|
|
27094
27711
|
if (command || manual.some((c) => c.length === words.length && c.every((w, n) => w === words[n])))
|
|
27095
27712
|
return { words, args: [...args, ...argv.slice(i + 1)], command };
|
|
27096
27713
|
}
|
|
@@ -27111,13 +27728,13 @@ var defaults = {
|
|
|
27111
27728
|
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
27112
27729
|
return Buffer.concat(chunks).toString("utf8");
|
|
27113
27730
|
},
|
|
27114
|
-
ask: () => new Promise((
|
|
27731
|
+
ask: () => new Promise((resolve2) => {
|
|
27115
27732
|
const input = createInterface({ input: process.stdin, terminal: false });
|
|
27116
27733
|
input.once("line", (line) => {
|
|
27117
|
-
|
|
27734
|
+
resolve2(/^(y|yes)$/i.test(line.trim()));
|
|
27118
27735
|
input.close();
|
|
27119
27736
|
});
|
|
27120
|
-
input.once("close", () =>
|
|
27737
|
+
input.once("close", () => resolve2(false));
|
|
27121
27738
|
}),
|
|
27122
27739
|
async openBrowser(url) {
|
|
27123
27740
|
try {
|
|
@@ -27125,15 +27742,35 @@ var defaults = {
|
|
|
27125
27742
|
if (!["https:", "http:"].includes(parsed.protocol))
|
|
27126
27743
|
return;
|
|
27127
27744
|
const cmd = process.platform === "darwin" ? ["open", url] : process.platform === "win32" ? ["rundll32", "url.dll,FileProtocolHandler", url] : ["xdg-open", url];
|
|
27128
|
-
await new Promise((
|
|
27745
|
+
await new Promise((resolve2, reject) => {
|
|
27129
27746
|
const child = spawn2(cmd[0], cmd.slice(1), { stdio: "ignore" });
|
|
27130
27747
|
child.once("error", reject);
|
|
27131
|
-
child.once("close", () =>
|
|
27748
|
+
child.once("close", () => resolve2());
|
|
27132
27749
|
});
|
|
27133
27750
|
} catch {}
|
|
27134
27751
|
},
|
|
27135
|
-
wait: (ms) => new Promise((
|
|
27136
|
-
render
|
|
27752
|
+
wait: (ms) => new Promise((resolve2) => setTimeout(resolve2, ms)),
|
|
27753
|
+
render,
|
|
27754
|
+
spawn: (cmd, args, options) => new Promise((resolve2, reject) => {
|
|
27755
|
+
const child = spawn2(cmd, args, { cwd: options.cwd, stdio: ["ignore", 2, 2] });
|
|
27756
|
+
child.once("error", reject);
|
|
27757
|
+
child.once("close", (code) => resolve2(code ?? 1));
|
|
27758
|
+
}),
|
|
27759
|
+
spawnCapture: (cmd, args, options) => new Promise((resolve2, reject) => {
|
|
27760
|
+
const child = spawn2(cmd, args, { cwd: options.cwd, stdio: ["ignore", "pipe", "pipe"] });
|
|
27761
|
+
const chunks = [];
|
|
27762
|
+
child.stdout.on("data", (c) => chunks.push(c));
|
|
27763
|
+
child.stderr.on("data", (c) => chunks.push(c));
|
|
27764
|
+
child.once("error", reject);
|
|
27765
|
+
child.once("close", (code) => resolve2({ code: code ?? 1, output: Buffer.concat(chunks).toString("utf8") }));
|
|
27766
|
+
}),
|
|
27767
|
+
has: (bin) => spawnSync(process.platform === "win32" ? "where" : "which", [bin], { stdio: "ignore" }).status === 0,
|
|
27768
|
+
cwd: process.cwd()
|
|
27769
|
+
};
|
|
27770
|
+
var appOptions = {
|
|
27771
|
+
"app create": [{ name: "dir", type: "string", description: "Directory to create the project in (default: ./<name>)." }, { name: "template", type: "string", description: "Copy a local template directory instead of downloading the standard template." }, { name: "no-install", type: "boolean", description: "Skip installing dependencies." }],
|
|
27772
|
+
"app deploy": [{ name: "dir", type: "string", description: "Project directory (default: the nearest parent containing praxis.jsonc)." }, { name: "no-build", type: "boolean", description: "Publish the existing dist/ output without building." }],
|
|
27773
|
+
"app rollback": [{ name: "dir", type: "string", description: "Project directory (default: the nearest parent containing praxis.jsonc)." }, { name: "to", type: "string", description: "Version id to deploy (default: the previous version)." }]
|
|
27137
27774
|
};
|
|
27138
27775
|
async function main(argv, injected = {}) {
|
|
27139
27776
|
const ports = { ...defaults, ...injected };
|
|
@@ -27141,6 +27778,8 @@ async function main(argv, injected = {}) {
|
|
|
27141
27778
|
const write = outputSession((result, context) => ports.render(redact(result, secrets), context));
|
|
27142
27779
|
if (ports.env.PRAXIS_API_KEY)
|
|
27143
27780
|
secrets.push(ports.env.PRAXIS_API_KEY);
|
|
27781
|
+
if (ports.env.PRAXIS_API_TOKEN)
|
|
27782
|
+
secrets.push(ports.env.PRAXIS_API_TOKEN);
|
|
27144
27783
|
let ctx = {
|
|
27145
27784
|
json: false,
|
|
27146
27785
|
...decideOutputMode({ json: false, stdoutIsTTY: ports.stdoutIsTTY, env: ports.env }),
|
|
@@ -27163,7 +27802,7 @@ async function main(argv, injected = {}) {
|
|
|
27163
27802
|
const selected = route(argv);
|
|
27164
27803
|
ctx.command = commandLine(selected.words);
|
|
27165
27804
|
ctx.resource = selected.words.at(-2) ?? selected.words[0];
|
|
27166
|
-
const specs = [...globalOptions, ...selected.command ? [...operationOptions(selected.command.op), ...selected.command.op.body ? [dataOption] : []] : selected.words
|
|
27805
|
+
const specs = [...globalOptions, ...selected.command ? [...operationOptions(selected.command.op), ...selected.command.op.body ? [dataOption] : []] : selected.words.join(" ") === API_WORD ? [dataOption] : selected.words[0] === "skill" ? [{ name: "install", type: "string" }] : appOptions[selected.words.join(" ")] ?? []];
|
|
27167
27806
|
const outputFlags = globalsOf(parseArgs(outputModeArgs(selected.args, specs), globalOptions));
|
|
27168
27807
|
ctx.json = Boolean(outputFlags.json);
|
|
27169
27808
|
Object.assign(ctx, decideOutputMode({ format: outputFlags.format, json: ctx.json, stdoutIsTTY: ports.stdoutIsTTY, env: ports.env }));
|
|
@@ -27177,15 +27816,22 @@ async function main(argv, injected = {}) {
|
|
|
27177
27816
|
return 0;
|
|
27178
27817
|
}
|
|
27179
27818
|
if (!selected.words.length || !selected.command && !manual.some((c) => c.join(" ") === selected.words.join(" "))) {
|
|
27180
|
-
if (selected.words.length && !commands.some((c) => selected.words.every((w, i) => c.words[i] === w)) && selected.words.join(" ")
|
|
27181
|
-
|
|
27819
|
+
if (selected.words.length && !commands.some((c) => selected.words.every((w, i) => c.words[i] === w)) && !workflowNouns.includes(selected.words.join(" "))) {
|
|
27820
|
+
const moved = movedTo(commands, selected.words);
|
|
27821
|
+
throw new UsageError(`Unknown command: ${selected.words.join(" ")}`, moved ? `Generated commands moved under \`praxis api\`. Did you mean: ${moved}?` : selected.words[0] === API_WORD ? "Run `praxis api --help` to list API domains." : "Run `praxis --help`.");
|
|
27822
|
+
}
|
|
27182
27823
|
if (parsed.positional.length)
|
|
27183
27824
|
throw new UsageError("Unexpected arguments");
|
|
27184
|
-
|
|
27185
|
-
|
|
27186
|
-
|
|
27187
|
-
|
|
27188
|
-
|
|
27825
|
+
const noun = selected.words.join(" ");
|
|
27826
|
+
emit({ kind: "message", channel: "stdout", text: workflowNouns.includes(noun) ? nounHelp(noun) : help(commands, selected.words) });
|
|
27827
|
+
return 0;
|
|
27828
|
+
}
|
|
27829
|
+
if (selected.words.join(" ") === API_WORD && !parsed.positional.length && !flags.help) {
|
|
27830
|
+
emit({ kind: "message", channel: "stdout", text: help(commands, [API_WORD]) });
|
|
27831
|
+
return 0;
|
|
27832
|
+
}
|
|
27833
|
+
if (flags.help && selected.words.join(" ") === API_WORD && !parsed.positional.length) {
|
|
27834
|
+
emit({ kind: "message", channel: "stdout", text: help(commands, [API_WORD]) });
|
|
27189
27835
|
return 0;
|
|
27190
27836
|
}
|
|
27191
27837
|
if (flags.help && selected.words[0] === "skill") {
|
|
@@ -27193,6 +27839,11 @@ Examples:
|
|
|
27193
27839
|
Print the AI skill or write <dir>/praxis-cli/SKILL.md.` });
|
|
27194
27840
|
return 0;
|
|
27195
27841
|
}
|
|
27842
|
+
const workflow = workflows.find((w) => w.example && w.words.join(" ") === selected.words.join(" "));
|
|
27843
|
+
if (flags.help && workflow) {
|
|
27844
|
+
emit({ kind: "message", channel: "stdout", text: workflowHelp(workflow, appOptions[workflow.words.join(" ")] ?? []) });
|
|
27845
|
+
return 0;
|
|
27846
|
+
}
|
|
27196
27847
|
if (flags.help) {
|
|
27197
27848
|
emit({ kind: "message", channel: "stdout", text: selected.command ? help(commands, selected.words, selected.command) : `praxis ${selected.words.join(" ")}${selected.words[0] === "api" ? " <METHOD> <path> [--data JSON|@file|-]" : selected.words[1] === "switch" ? " <organization_id>" : ""}
|
|
27198
27849
|
${globalOptions.map((s) => "--" + s.name).join(" ")}
|
|
@@ -27286,7 +27937,7 @@ Examples:
|
|
|
27286
27937
|
return 0;
|
|
27287
27938
|
}
|
|
27288
27939
|
const name = selected.words.join(" ");
|
|
27289
|
-
const arity = name === "api" ? 2 : name === "org switch" ? 1 : 0;
|
|
27940
|
+
const arity = name === "api" ? 2 : name === "org switch" || name === "app create" ? 1 : 0;
|
|
27290
27941
|
if (parsed.positional.length !== arity)
|
|
27291
27942
|
throw new UsageError(`${name} expects ${arity} positional argument(s)`);
|
|
27292
27943
|
if (!quiet)
|
|
@@ -27318,8 +27969,29 @@ Examples:
|
|
|
27318
27969
|
case "org switch":
|
|
27319
27970
|
emit({ kind: "object", data: await auth.switchOrganization(parsed.positional[0]) });
|
|
27320
27971
|
break;
|
|
27972
|
+
case "app create": {
|
|
27973
|
+
const appPorts = { ...ports, mode: ctx.mode, emit, human: (text) => {
|
|
27974
|
+
if (ctx.mode === "human")
|
|
27975
|
+
emit({ kind: "message", channel: "stdout", text });
|
|
27976
|
+
}, random: randomSecret };
|
|
27977
|
+
await appCreate({ name: parsed.positional[0], dir: parsed.flags.dir?.[0], template: parsed.flags.template?.[0], install: !parsed.flags["no-install"] }, config.env, appPorts);
|
|
27978
|
+
break;
|
|
27979
|
+
}
|
|
27980
|
+
case "app deploy":
|
|
27981
|
+
case "app rollback": {
|
|
27982
|
+
const appPorts = { ...ports, mode: ctx.mode, emit, human: () => {}, random: randomSecret };
|
|
27983
|
+
const client2 = await auth.client(org);
|
|
27984
|
+
const dir = parsed.flags.dir?.[0];
|
|
27985
|
+
if (name === "app deploy")
|
|
27986
|
+
await appDeploy({ dir, build: !parsed.flags["no-build"] }, client2, appPorts);
|
|
27987
|
+
else
|
|
27988
|
+
await appRollback({ dir, to: parsed.flags.to?.[0] }, client2, appPorts);
|
|
27989
|
+
break;
|
|
27990
|
+
}
|
|
27321
27991
|
case "api": {
|
|
27322
|
-
const method = parsed.positional[0]
|
|
27992
|
+
const method = parsed.positional[0];
|
|
27993
|
+
if (!HTTP_METHOD.test(method))
|
|
27994
|
+
throw new UsageError(`Unknown command: api ${method}`, "Run `praxis api --help` to list API domains; raw requests use an uppercase method, such as `praxis api GET /model/v1/models`.");
|
|
27323
27995
|
await confirm(method.toLowerCase());
|
|
27324
27996
|
if (!quiet) {
|
|
27325
27997
|
stopProgress();
|